├── .gitignore ├── LICENSE ├── README.md ├── circle.yml ├── package-lock.json ├── package.json ├── src ├── crypto.js └── parse-mockdb.js └── test └── test.js /.gitignore: -------------------------------------------------------------------------------- 1 | .idea/ 2 | /node_modules 3 | *.log 4 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) [2014] [Elliot Hesp] 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. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Parse MockDB 2 | ===================== 3 | 4 | Master Build Status: [![Circle CI](https://circleci.com/gh/Hustle/parse-mockdb/tree/master.svg?style=svg)](https://circleci.com/gh/Hustle/parse-mockdb/tree/master) 5 | 6 | Provides a mocked Parse RESTController compatible with version `2.0+` of the JavaScript SDK. 7 | 8 | ### Installation and Usage 9 | 10 | ```js 11 | npm install parse-mockdb --save-dev 12 | ``` 13 | 14 | ```js 15 | 'use strict'; 16 | const Parse = require('parse-shim'); 17 | const ParseMockDB = require('parse-mockdb'); 18 | 19 | ParseMockDB.mockDB(Parse); // Mock the Parse RESTController 20 | 21 | // Perform saves, queries, updates, deletes, etc... using the Parse JS SDK 22 | 23 | ParseMockDB.cleanUp(); // Clear the Database 24 | ParseMockDB.unMockDB(); // Un-mock the Parse RESTController 25 | ``` 26 | 27 | ### Completeness 28 | 29 | - [x] Basic CRUD (save, destroy, fetch) 30 | - [x] Query operators ($exists, $in, $nin, $eq, $ne, $lt, $lte, $gt, $gte, $regex, $select, $inQuery, $all, $nearSphere) 31 | - [x] Update operators (Increment, Add, AddUnique, Remove, Delete) 32 | - [x] Parse.Relation (AddRelation, RemoveRelation) 33 | - [x] Parse query dotted notation matching eg `{ "name.first": "Tyler" })` 34 | - [ ] Parse class level permissions 35 | - [ ] Parse.ACL (row level permissions) 36 | - [ ] Parse special classes (Parse.User, Parse.Role, ...) 37 | - [ ] Parse lifecycle hooks (beforeSave - done, afterSave - done, beforeDelete - done, afterDelete) 38 | 39 | 40 | ### Changelog 41 | 42 | ### v0.4.0 43 | 44 | - *Breaking Change* This library is now targeting the 2.x series of the Parse JS SDK. If you are 45 | using Parse 1.6+, you should pin to the v0.3.x release. 46 | 47 | #### v0.3.0 48 | - *Breaking Change* When calling `mockDB()` you must now pass in a reference to 49 | the Parse SDK that you want to mock. 50 | 51 | - *Breaking Change* Stopped patching MockDB object on to Parse module. You can no longer 52 | access `Parse.MockDB`, you must load the `parse-mockdb` module explicitly. 53 | 54 | - *Breaking Change* Removed ParseMockDB.promiseResultSync method 55 | 56 | 57 | ### Tests 58 | 59 | ```sh 60 | npm test 61 | ``` 62 | -------------------------------------------------------------------------------- /circle.yml: -------------------------------------------------------------------------------- 1 | machine: 2 | node: 3 | version: 6.0.0 4 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-mockdb", 3 | "version": "0.4.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "@babel/runtime": { 8 | "version": "7.7.7", 9 | "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.7.7.tgz", 10 | "integrity": "sha512-uCnC2JEVAu8AKB5do1WRIsvrdJ0flYx/A/9f/6chdacnEZ7LmavjdsDXr5ksYBegxtuTPR5Va9/+13QF/kFkCA==", 11 | "dev": true, 12 | "requires": { 13 | "regenerator-runtime": "^0.13.2" 14 | } 15 | }, 16 | "@babel/runtime-corejs3": { 17 | "version": "7.7.7", 18 | "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.7.7.tgz", 19 | "integrity": "sha512-kr3W3Fw8mB/CTru2M5zIRQZZgC/9zOxNSoJ/tVCzjPt3H1/p5uuGbz6WwmaQy/TLQcW31rUhUUWKY28sXFRelA==", 20 | "dev": true, 21 | "requires": { 22 | "core-js-pure": "^3.0.0", 23 | "regenerator-runtime": "^0.13.2" 24 | } 25 | }, 26 | "acorn": { 27 | "version": "5.1.1", 28 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-5.1.1.tgz", 29 | "integrity": "sha512-vOk6uEMctu0vQrvuSqFdJyqj1Q0S5VTDL79qtjo+DhRr+1mmaD+tluFSCZqhvi/JUhXSzoZN2BhtstaPEeE8cw==", 30 | "dev": true 31 | }, 32 | "acorn-jsx": { 33 | "version": "3.0.1", 34 | "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-3.0.1.tgz", 35 | "integrity": "sha1-r9+UiPsezvyDSPb7IvRk4ypYs2s=", 36 | "dev": true, 37 | "requires": { 38 | "acorn": "^3.0.4" 39 | }, 40 | "dependencies": { 41 | "acorn": { 42 | "version": "3.3.0", 43 | "resolved": "https://registry.npmjs.org/acorn/-/acorn-3.3.0.tgz", 44 | "integrity": "sha1-ReN/s56No/JbruP/U2niu18iAXo=", 45 | "dev": true 46 | } 47 | } 48 | }, 49 | "ajv": { 50 | "version": "4.11.8", 51 | "resolved": "https://registry.npmjs.org/ajv/-/ajv-4.11.8.tgz", 52 | "integrity": "sha1-gv+wKynmYq5TvcIK8VlHcGc5xTY=", 53 | "dev": true, 54 | "requires": { 55 | "co": "^4.6.0", 56 | "json-stable-stringify": "^1.0.1" 57 | } 58 | }, 59 | "ajv-keywords": { 60 | "version": "1.5.1", 61 | "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-1.5.1.tgz", 62 | "integrity": "sha1-MU3QpLM2j609/NxU7eYXG4htrzw=", 63 | "dev": true 64 | }, 65 | "ansi-escapes": { 66 | "version": "1.4.0", 67 | "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-1.4.0.tgz", 68 | "integrity": "sha1-06ioOzGapneTZisT52HHkRQiMG4=", 69 | "dev": true 70 | }, 71 | "ansi-regex": { 72 | "version": "2.1.1", 73 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.1.1.tgz", 74 | "integrity": "sha1-w7M6te42DYbg5ijwRorn7yfWVN8=", 75 | "dev": true 76 | }, 77 | "ansi-styles": { 78 | "version": "2.2.1", 79 | "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz", 80 | "integrity": "sha1-tDLdM1i2NM914eRmQ2gkBTPB3b4=", 81 | "dev": true 82 | }, 83 | "argparse": { 84 | "version": "1.0.9", 85 | "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.9.tgz", 86 | "integrity": "sha1-c9g7wmP4bpf4zE9rrhsOkKfSLIY=", 87 | "dev": true, 88 | "requires": { 89 | "sprintf-js": "~1.0.2" 90 | } 91 | }, 92 | "array-union": { 93 | "version": "1.0.2", 94 | "resolved": "https://registry.npmjs.org/array-union/-/array-union-1.0.2.tgz", 95 | "integrity": "sha1-mjRBDk9OPaI96jdb5b5w8kd47Dk=", 96 | "dev": true, 97 | "requires": { 98 | "array-uniq": "^1.0.1" 99 | } 100 | }, 101 | "array-uniq": { 102 | "version": "1.0.3", 103 | "resolved": "https://registry.npmjs.org/array-uniq/-/array-uniq-1.0.3.tgz", 104 | "integrity": "sha1-r2rId6Jcx/dOBYiUdThY39sk/bY=", 105 | "dev": true 106 | }, 107 | "array.prototype.find": { 108 | "version": "2.0.4", 109 | "resolved": "https://registry.npmjs.org/array.prototype.find/-/array.prototype.find-2.0.4.tgz", 110 | "integrity": "sha1-VWpcU2LAhkgyPdrrnenRS8GGTJA=", 111 | "dev": true, 112 | "requires": { 113 | "define-properties": "^1.1.2", 114 | "es-abstract": "^1.7.0" 115 | } 116 | }, 117 | "arrify": { 118 | "version": "1.0.1", 119 | "resolved": "https://registry.npmjs.org/arrify/-/arrify-1.0.1.tgz", 120 | "integrity": "sha1-iYUI2iIm84DfkEcoRWhJwVAaSw0=", 121 | "dev": true 122 | }, 123 | "babel-code-frame": { 124 | "version": "6.22.0", 125 | "resolved": "https://registry.npmjs.org/babel-code-frame/-/babel-code-frame-6.22.0.tgz", 126 | "integrity": "sha1-AnYgvuVnqIwyVhV05/0IAdMxGOQ=", 127 | "dev": true, 128 | "requires": { 129 | "chalk": "^1.1.0", 130 | "esutils": "^2.0.2", 131 | "js-tokens": "^3.0.0" 132 | } 133 | }, 134 | "balanced-match": { 135 | "version": "1.0.0", 136 | "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.0.tgz", 137 | "integrity": "sha1-ibTRmasr7kneFk6gK4nORi1xt2c=", 138 | "dev": true 139 | }, 140 | "brace-expansion": { 141 | "version": "1.1.8", 142 | "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.8.tgz", 143 | "integrity": "sha1-wHshHHyVLsH479Uad+8NHTmQopI=", 144 | "dev": true, 145 | "requires": { 146 | "balanced-match": "^1.0.0", 147 | "concat-map": "0.0.1" 148 | } 149 | }, 150 | "builtin-modules": { 151 | "version": "1.1.1", 152 | "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-1.1.1.tgz", 153 | "integrity": "sha1-Jw8HbFpywC9bZaR9+Uxf46J4iS8=", 154 | "dev": true 155 | }, 156 | "caller-path": { 157 | "version": "0.1.0", 158 | "resolved": "https://registry.npmjs.org/caller-path/-/caller-path-0.1.0.tgz", 159 | "integrity": "sha1-lAhe9jWB7NPaqSREqP6U6CV3dR8=", 160 | "dev": true, 161 | "requires": { 162 | "callsites": "^0.2.0" 163 | } 164 | }, 165 | "callsites": { 166 | "version": "0.2.0", 167 | "resolved": "https://registry.npmjs.org/callsites/-/callsites-0.2.0.tgz", 168 | "integrity": "sha1-r6uWJikQp/M8GaV3WCXGnzTjUMo=", 169 | "dev": true 170 | }, 171 | "chalk": { 172 | "version": "1.1.3", 173 | "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz", 174 | "integrity": "sha1-qBFcVeSnAv5NFQq9OHKCKn4J/Jg=", 175 | "dev": true, 176 | "requires": { 177 | "ansi-styles": "^2.2.1", 178 | "escape-string-regexp": "^1.0.2", 179 | "has-ansi": "^2.0.0", 180 | "strip-ansi": "^3.0.0", 181 | "supports-color": "^2.0.0" 182 | } 183 | }, 184 | "circular-json": { 185 | "version": "0.3.3", 186 | "resolved": "https://registry.npmjs.org/circular-json/-/circular-json-0.3.3.tgz", 187 | "integrity": "sha512-UZK3NBx2Mca+b5LsG7bY183pHWt5Y1xts4P3Pz7ENTwGVnJOUWbRb3ocjvX7hx9tq/yTAdclXm9sZ38gNuem4A==", 188 | "dev": true 189 | }, 190 | "cli-cursor": { 191 | "version": "1.0.2", 192 | "resolved": "https://registry.npmjs.org/cli-cursor/-/cli-cursor-1.0.2.tgz", 193 | "integrity": "sha1-ZNo/fValRBLll5S9Ytw1KV6PKYc=", 194 | "dev": true, 195 | "requires": { 196 | "restore-cursor": "^1.0.1" 197 | } 198 | }, 199 | "cli-width": { 200 | "version": "2.1.0", 201 | "resolved": "https://registry.npmjs.org/cli-width/-/cli-width-2.1.0.tgz", 202 | "integrity": "sha1-sjTKIJsp72b8UY2bmNWEewDt8Ao=", 203 | "dev": true 204 | }, 205 | "co": { 206 | "version": "4.6.0", 207 | "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", 208 | "integrity": "sha1-bqa989hTrlTMuOR7+gvz+QMfsYQ=", 209 | "dev": true 210 | }, 211 | "code-point-at": { 212 | "version": "1.1.0", 213 | "resolved": "https://registry.npmjs.org/code-point-at/-/code-point-at-1.1.0.tgz", 214 | "integrity": "sha1-DQcLTQQ6W+ozovGkDi7bPZpMz3c=", 215 | "dev": true 216 | }, 217 | "commander": { 218 | "version": "2.3.0", 219 | "resolved": "https://registry.npmjs.org/commander/-/commander-2.3.0.tgz", 220 | "integrity": "sha1-/UMOiJgy7DU7ms0d4hfBHLPu+HM=", 221 | "dev": true 222 | }, 223 | "concat-map": { 224 | "version": "0.0.1", 225 | "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", 226 | "integrity": "sha1-2Klr13/Wjfd5OnMDajug1UBdR3s=", 227 | "dev": true 228 | }, 229 | "concat-stream": { 230 | "version": "1.6.0", 231 | "resolved": "https://registry.npmjs.org/concat-stream/-/concat-stream-1.6.0.tgz", 232 | "integrity": "sha1-CqxmL9Ur54lk1VMvaUeE5wEQrPc=", 233 | "dev": true, 234 | "requires": { 235 | "inherits": "^2.0.3", 236 | "readable-stream": "^2.2.2", 237 | "typedarray": "^0.0.6" 238 | } 239 | }, 240 | "contains-path": { 241 | "version": "0.1.0", 242 | "resolved": "https://registry.npmjs.org/contains-path/-/contains-path-0.1.0.tgz", 243 | "integrity": "sha1-/ozxhP9mcLa67wGp1IYaXL7EEgo=", 244 | "dev": true 245 | }, 246 | "core-js-pure": { 247 | "version": "3.6.4", 248 | "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.6.4.tgz", 249 | "integrity": "sha512-epIhRLkXdgv32xIUFaaAry2wdxZYBi6bgM7cB136dzzXXa+dFyRLTZeLUJxnd8ShrmyVXBub63n2NHo2JAt8Cw==", 250 | "dev": true 251 | }, 252 | "core-util-is": { 253 | "version": "1.0.2", 254 | "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz", 255 | "integrity": "sha1-tf1UIgqivFq1eqtxQMlAdUUDwac=", 256 | "dev": true 257 | }, 258 | "crypto-js": { 259 | "version": "3.1.9-1", 260 | "resolved": "https://registry.npmjs.org/crypto-js/-/crypto-js-3.1.9-1.tgz", 261 | "integrity": "sha1-/aGedh/Ad+Af+/3G6f38WeiAbNg=", 262 | "dev": true 263 | }, 264 | "d": { 265 | "version": "1.0.0", 266 | "resolved": "https://registry.npmjs.org/d/-/d-1.0.0.tgz", 267 | "integrity": "sha1-dUu1v+VUUdpppYuU1F9MWwRi1Y8=", 268 | "dev": true, 269 | "requires": { 270 | "es5-ext": "^0.10.9" 271 | } 272 | }, 273 | "damerau-levenshtein": { 274 | "version": "1.0.4", 275 | "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.4.tgz", 276 | "integrity": "sha1-AxkcQyy27qFou3fzpV/9zLiXhRQ=", 277 | "dev": true 278 | }, 279 | "debug": { 280 | "version": "2.6.8", 281 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.8.tgz", 282 | "integrity": "sha1-5zFTHKLt4n0YgiJCfaF4IdaP9Pw=", 283 | "dev": true, 284 | "requires": { 285 | "ms": "2.0.0" 286 | } 287 | }, 288 | "deep-is": { 289 | "version": "0.1.3", 290 | "resolved": "https://registry.npmjs.org/deep-is/-/deep-is-0.1.3.tgz", 291 | "integrity": "sha1-s2nW+128E+7PUk+RsHD+7cNXzzQ=", 292 | "dev": true 293 | }, 294 | "define-properties": { 295 | "version": "1.1.2", 296 | "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.1.2.tgz", 297 | "integrity": "sha1-g6c/L+pWmJj7c3GTyPhzyvbUXJQ=", 298 | "dev": true, 299 | "requires": { 300 | "foreach": "^2.0.5", 301 | "object-keys": "^1.0.8" 302 | } 303 | }, 304 | "del": { 305 | "version": "2.2.2", 306 | "resolved": "https://registry.npmjs.org/del/-/del-2.2.2.tgz", 307 | "integrity": "sha1-wSyYHQZ4RshLyvhiz/kw2Qf/0ag=", 308 | "dev": true, 309 | "requires": { 310 | "globby": "^5.0.0", 311 | "is-path-cwd": "^1.0.0", 312 | "is-path-in-cwd": "^1.0.0", 313 | "object-assign": "^4.0.1", 314 | "pify": "^2.0.0", 315 | "pinkie-promise": "^2.0.0", 316 | "rimraf": "^2.2.8" 317 | } 318 | }, 319 | "diff": { 320 | "version": "1.4.0", 321 | "resolved": "https://registry.npmjs.org/diff/-/diff-1.4.0.tgz", 322 | "integrity": "sha1-fyjS657nsVqX79ic5j3P2qPMur8=", 323 | "dev": true 324 | }, 325 | "doctrine": { 326 | "version": "2.0.0", 327 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.0.0.tgz", 328 | "integrity": "sha1-xz2NKQnSIpHhoAejlYBNqLZl/mM=", 329 | "dev": true, 330 | "requires": { 331 | "esutils": "^2.0.2", 332 | "isarray": "^1.0.0" 333 | } 334 | }, 335 | "es-abstract": { 336 | "version": "1.8.0", 337 | "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.8.0.tgz", 338 | "integrity": "sha512-Cf9/h5MrXtExM20gSS55YFrGKCyPrRBjIVBtVyy8vmlsDfe0NPKMWj65tPLgzyfPuapWxh5whpXCtW4+AW5mRg==", 339 | "dev": true, 340 | "requires": { 341 | "es-to-primitive": "^1.1.1", 342 | "function-bind": "^1.1.0", 343 | "has": "^1.0.1", 344 | "is-callable": "^1.1.3", 345 | "is-regex": "^1.0.4" 346 | } 347 | }, 348 | "es-to-primitive": { 349 | "version": "1.1.1", 350 | "resolved": "https://registry.npmjs.org/es-to-primitive/-/es-to-primitive-1.1.1.tgz", 351 | "integrity": "sha1-RTVSSKiJeQNLZ5Lhm7gfK3l13Q0=", 352 | "dev": true, 353 | "requires": { 354 | "is-callable": "^1.1.1", 355 | "is-date-object": "^1.0.1", 356 | "is-symbol": "^1.0.1" 357 | } 358 | }, 359 | "es5-ext": { 360 | "version": "0.10.27", 361 | "resolved": "https://registry.npmjs.org/es5-ext/-/es5-ext-0.10.27.tgz", 362 | "integrity": "sha512-3KXJRYzKXTd7xfFy5uZsJCXue55fAYQ035PRjyYk2PicllxIwcW9l3AbM/eGaw3vgVAUW4tl4xg9AXDEI6yw0w==", 363 | "dev": true, 364 | "requires": { 365 | "es6-iterator": "2", 366 | "es6-symbol": "~3.1" 367 | } 368 | }, 369 | "es6-iterator": { 370 | "version": "2.0.1", 371 | "resolved": "https://registry.npmjs.org/es6-iterator/-/es6-iterator-2.0.1.tgz", 372 | "integrity": "sha1-jjGcnwRTv1ddN0lAplWSDlnKVRI=", 373 | "dev": true, 374 | "requires": { 375 | "d": "1", 376 | "es5-ext": "^0.10.14", 377 | "es6-symbol": "^3.1" 378 | } 379 | }, 380 | "es6-map": { 381 | "version": "0.1.5", 382 | "resolved": "https://registry.npmjs.org/es6-map/-/es6-map-0.1.5.tgz", 383 | "integrity": "sha1-kTbgUD3MBqMBaQ8LsU/042TpSfA=", 384 | "dev": true, 385 | "requires": { 386 | "d": "1", 387 | "es5-ext": "~0.10.14", 388 | "es6-iterator": "~2.0.1", 389 | "es6-set": "~0.1.5", 390 | "es6-symbol": "~3.1.1", 391 | "event-emitter": "~0.3.5" 392 | } 393 | }, 394 | "es6-set": { 395 | "version": "0.1.5", 396 | "resolved": "https://registry.npmjs.org/es6-set/-/es6-set-0.1.5.tgz", 397 | "integrity": "sha1-0rPsXU2ADO2BjbU40ol02wpzzLE=", 398 | "dev": true, 399 | "requires": { 400 | "d": "1", 401 | "es5-ext": "~0.10.14", 402 | "es6-iterator": "~2.0.1", 403 | "es6-symbol": "3.1.1", 404 | "event-emitter": "~0.3.5" 405 | } 406 | }, 407 | "es6-symbol": { 408 | "version": "3.1.1", 409 | "resolved": "https://registry.npmjs.org/es6-symbol/-/es6-symbol-3.1.1.tgz", 410 | "integrity": "sha1-vwDvT9q2uhtG7Le2KbTH7VcVzHc=", 411 | "dev": true, 412 | "requires": { 413 | "d": "1", 414 | "es5-ext": "~0.10.14" 415 | } 416 | }, 417 | "es6-weak-map": { 418 | "version": "2.0.2", 419 | "resolved": "https://registry.npmjs.org/es6-weak-map/-/es6-weak-map-2.0.2.tgz", 420 | "integrity": "sha1-XjqzIlH/0VOKH45f+hNXdy+S2W8=", 421 | "dev": true, 422 | "requires": { 423 | "d": "1", 424 | "es5-ext": "^0.10.14", 425 | "es6-iterator": "^2.0.1", 426 | "es6-symbol": "^3.1.1" 427 | } 428 | }, 429 | "escape-string-regexp": { 430 | "version": "1.0.5", 431 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", 432 | "integrity": "sha1-G2HAViGQqN/2rjuyzwIAyhMLhtQ=", 433 | "dev": true 434 | }, 435 | "escope": { 436 | "version": "3.6.0", 437 | "resolved": "https://registry.npmjs.org/escope/-/escope-3.6.0.tgz", 438 | "integrity": "sha1-4Bl16BJ4GhY6ba392AOY3GTIicM=", 439 | "dev": true, 440 | "requires": { 441 | "es6-map": "^0.1.3", 442 | "es6-weak-map": "^2.0.1", 443 | "esrecurse": "^4.1.0", 444 | "estraverse": "^4.1.1" 445 | } 446 | }, 447 | "eslint": { 448 | "version": "3.19.0", 449 | "resolved": "https://registry.npmjs.org/eslint/-/eslint-3.19.0.tgz", 450 | "integrity": "sha1-yPxiAcf0DdCJQbh8CFdnOGpnmsw=", 451 | "dev": true, 452 | "requires": { 453 | "babel-code-frame": "^6.16.0", 454 | "chalk": "^1.1.3", 455 | "concat-stream": "^1.5.2", 456 | "debug": "^2.1.1", 457 | "doctrine": "^2.0.0", 458 | "escope": "^3.6.0", 459 | "espree": "^3.4.0", 460 | "esquery": "^1.0.0", 461 | "estraverse": "^4.2.0", 462 | "esutils": "^2.0.2", 463 | "file-entry-cache": "^2.0.0", 464 | "glob": "^7.0.3", 465 | "globals": "^9.14.0", 466 | "ignore": "^3.2.0", 467 | "imurmurhash": "^0.1.4", 468 | "inquirer": "^0.12.0", 469 | "is-my-json-valid": "^2.10.0", 470 | "is-resolvable": "^1.0.0", 471 | "js-yaml": "^3.5.1", 472 | "json-stable-stringify": "^1.0.0", 473 | "levn": "^0.3.0", 474 | "lodash": "^4.0.0", 475 | "mkdirp": "^0.5.0", 476 | "natural-compare": "^1.4.0", 477 | "optionator": "^0.8.2", 478 | "path-is-inside": "^1.0.1", 479 | "pluralize": "^1.2.1", 480 | "progress": "^1.1.8", 481 | "require-uncached": "^1.0.2", 482 | "shelljs": "^0.7.5", 483 | "strip-bom": "^3.0.0", 484 | "strip-json-comments": "~2.0.1", 485 | "table": "^3.7.8", 486 | "text-table": "~0.2.0", 487 | "user-home": "^2.0.0" 488 | } 489 | }, 490 | "eslint-config-airbnb": { 491 | "version": "10.0.1", 492 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb/-/eslint-config-airbnb-10.0.1.tgz", 493 | "integrity": "sha1-pHAQhkbWxF4fY5oD8R1QShqkrtw=", 494 | "dev": true, 495 | "requires": { 496 | "eslint-config-airbnb-base": "^5.0.2" 497 | } 498 | }, 499 | "eslint-config-airbnb-base": { 500 | "version": "5.0.3", 501 | "resolved": "https://registry.npmjs.org/eslint-config-airbnb-base/-/eslint-config-airbnb-base-5.0.3.tgz", 502 | "integrity": "sha1-lxSsNews1/qw1E0Uip+R2ylEB00=", 503 | "dev": true 504 | }, 505 | "eslint-import-resolver-node": { 506 | "version": "0.2.3", 507 | "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.2.3.tgz", 508 | "integrity": "sha1-Wt2BBujJKNssuiMrzZ76hG49oWw=", 509 | "dev": true, 510 | "requires": { 511 | "debug": "^2.2.0", 512 | "object-assign": "^4.0.1", 513 | "resolve": "^1.1.6" 514 | } 515 | }, 516 | "eslint-plugin-import": { 517 | "version": "1.16.0", 518 | "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-1.16.0.tgz", 519 | "integrity": "sha1-svoH68xTUE0PKkR3WC7Iv/GHG58=", 520 | "dev": true, 521 | "requires": { 522 | "builtin-modules": "^1.1.1", 523 | "contains-path": "^0.1.0", 524 | "debug": "^2.2.0", 525 | "doctrine": "1.3.x", 526 | "es6-map": "^0.1.3", 527 | "es6-set": "^0.1.4", 528 | "eslint-import-resolver-node": "^0.2.0", 529 | "has": "^1.0.1", 530 | "lodash.cond": "^4.3.0", 531 | "lodash.endswith": "^4.0.1", 532 | "lodash.find": "^4.3.0", 533 | "lodash.findindex": "^4.3.0", 534 | "minimatch": "^3.0.3", 535 | "object-assign": "^4.0.1", 536 | "pkg-dir": "^1.0.0", 537 | "pkg-up": "^1.0.0" 538 | }, 539 | "dependencies": { 540 | "doctrine": { 541 | "version": "1.3.0", 542 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.3.0.tgz", 543 | "integrity": "sha1-E+dWgrVVGEJCdvfBc3g0Vu+RPSY=", 544 | "dev": true, 545 | "requires": { 546 | "esutils": "^2.0.2", 547 | "isarray": "^1.0.0" 548 | } 549 | } 550 | } 551 | }, 552 | "eslint-plugin-jsx-a11y": { 553 | "version": "2.2.3", 554 | "resolved": "https://registry.npmjs.org/eslint-plugin-jsx-a11y/-/eslint-plugin-jsx-a11y-2.2.3.tgz", 555 | "integrity": "sha1-TjXLcbin23AqxBXIBuuOjZ6mxl0=", 556 | "dev": true, 557 | "requires": { 558 | "damerau-levenshtein": "^1.0.0", 559 | "jsx-ast-utils": "^1.0.0", 560 | "object-assign": "^4.0.1" 561 | } 562 | }, 563 | "eslint-plugin-react": { 564 | "version": "6.10.3", 565 | "resolved": "https://registry.npmjs.org/eslint-plugin-react/-/eslint-plugin-react-6.10.3.tgz", 566 | "integrity": "sha1-xUNb6wZ3ThLH2y9qut3L+QDNP3g=", 567 | "dev": true, 568 | "requires": { 569 | "array.prototype.find": "^2.0.1", 570 | "doctrine": "^1.2.2", 571 | "has": "^1.0.1", 572 | "jsx-ast-utils": "^1.3.4", 573 | "object.assign": "^4.0.4" 574 | }, 575 | "dependencies": { 576 | "doctrine": { 577 | "version": "1.5.0", 578 | "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-1.5.0.tgz", 579 | "integrity": "sha1-N53Ocw9hZvds76TmcHoVmwLFpvo=", 580 | "dev": true, 581 | "requires": { 582 | "esutils": "^2.0.2", 583 | "isarray": "^1.0.0" 584 | } 585 | } 586 | } 587 | }, 588 | "espree": { 589 | "version": "3.5.0", 590 | "resolved": "https://registry.npmjs.org/espree/-/espree-3.5.0.tgz", 591 | "integrity": "sha1-mDWGJb3QVYYeon4oZ+pyn69GPY0=", 592 | "dev": true, 593 | "requires": { 594 | "acorn": "^5.1.1", 595 | "acorn-jsx": "^3.0.0" 596 | } 597 | }, 598 | "esprima": { 599 | "version": "4.0.0", 600 | "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.0.tgz", 601 | "integrity": "sha512-oftTcaMu/EGrEIu904mWteKIv8vMuOgGYo7EhVJJN00R/EED9DCua/xxHRdYnKtcECzVg7xOWhflvJMnqcFZjw==", 602 | "dev": true 603 | }, 604 | "esquery": { 605 | "version": "1.0.0", 606 | "resolved": "https://registry.npmjs.org/esquery/-/esquery-1.0.0.tgz", 607 | "integrity": "sha1-z7qLV9f7qT8XKYqKAGoEzaE9gPo=", 608 | "dev": true, 609 | "requires": { 610 | "estraverse": "^4.0.0" 611 | } 612 | }, 613 | "esrecurse": { 614 | "version": "4.2.0", 615 | "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.2.0.tgz", 616 | "integrity": "sha1-+pVo2Y04I/mkHZHpAtyrnqblsWM=", 617 | "dev": true, 618 | "requires": { 619 | "estraverse": "^4.1.0", 620 | "object-assign": "^4.0.1" 621 | } 622 | }, 623 | "estraverse": { 624 | "version": "4.2.0", 625 | "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.2.0.tgz", 626 | "integrity": "sha1-De4/7TH81GlhjOc0IJn8GvoL2xM=", 627 | "dev": true 628 | }, 629 | "esutils": { 630 | "version": "2.0.2", 631 | "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.2.tgz", 632 | "integrity": "sha1-Cr9PHKpbyx96nYrMbepPqqBLrJs=", 633 | "dev": true 634 | }, 635 | "event-emitter": { 636 | "version": "0.3.5", 637 | "resolved": "https://registry.npmjs.org/event-emitter/-/event-emitter-0.3.5.tgz", 638 | "integrity": "sha1-34xp7vFkeSPHFXuc6DhAYQsCzDk=", 639 | "dev": true, 640 | "requires": { 641 | "d": "1", 642 | "es5-ext": "~0.10.14" 643 | } 644 | }, 645 | "exit-hook": { 646 | "version": "1.1.1", 647 | "resolved": "https://registry.npmjs.org/exit-hook/-/exit-hook-1.1.1.tgz", 648 | "integrity": "sha1-8FyiM7SMBdVP/wd2XfhQfpXAL/g=", 649 | "dev": true 650 | }, 651 | "fast-levenshtein": { 652 | "version": "2.0.6", 653 | "resolved": "https://registry.npmjs.org/fast-levenshtein/-/fast-levenshtein-2.0.6.tgz", 654 | "integrity": "sha1-PYpcZog6FqMMqGQ+hR8Zuqd5eRc=", 655 | "dev": true 656 | }, 657 | "figures": { 658 | "version": "1.7.0", 659 | "resolved": "https://registry.npmjs.org/figures/-/figures-1.7.0.tgz", 660 | "integrity": "sha1-y+Hjr/zxzUS4DK3+0o3Hk6lwHS4=", 661 | "dev": true, 662 | "requires": { 663 | "escape-string-regexp": "^1.0.5", 664 | "object-assign": "^4.1.0" 665 | } 666 | }, 667 | "file-entry-cache": { 668 | "version": "2.0.0", 669 | "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-2.0.0.tgz", 670 | "integrity": "sha1-w5KZDD5oR4PYOLjISkXYoEhFg2E=", 671 | "dev": true, 672 | "requires": { 673 | "flat-cache": "^1.2.1", 674 | "object-assign": "^4.0.1" 675 | } 676 | }, 677 | "find-up": { 678 | "version": "1.1.2", 679 | "resolved": "https://registry.npmjs.org/find-up/-/find-up-1.1.2.tgz", 680 | "integrity": "sha1-ay6YIrGizgpgq2TWEOzK1TyyTQ8=", 681 | "dev": true, 682 | "requires": { 683 | "path-exists": "^2.0.0", 684 | "pinkie-promise": "^2.0.0" 685 | } 686 | }, 687 | "flat-cache": { 688 | "version": "1.2.2", 689 | "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-1.2.2.tgz", 690 | "integrity": "sha1-+oZxTnLCHbiGAXYezy9VXRq8a5Y=", 691 | "dev": true, 692 | "requires": { 693 | "circular-json": "^0.3.1", 694 | "del": "^2.0.2", 695 | "graceful-fs": "^4.1.2", 696 | "write": "^0.2.1" 697 | } 698 | }, 699 | "foreach": { 700 | "version": "2.0.5", 701 | "resolved": "https://registry.npmjs.org/foreach/-/foreach-2.0.5.tgz", 702 | "integrity": "sha1-C+4AUBiusmDQo6865ljdATbsG5k=", 703 | "dev": true 704 | }, 705 | "fs.realpath": { 706 | "version": "1.0.0", 707 | "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", 708 | "integrity": "sha1-FQStJSMVjKpA20onh8sBQRmU6k8=", 709 | "dev": true 710 | }, 711 | "function-bind": { 712 | "version": "1.1.0", 713 | "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.0.tgz", 714 | "integrity": "sha1-FhdnFMgBeY5Ojyz391KUZ7tKV3E=", 715 | "dev": true 716 | }, 717 | "generate-function": { 718 | "version": "2.0.0", 719 | "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz", 720 | "integrity": "sha1-aFj+fAlpt9TpCTM3ZHrHn2DfvnQ=", 721 | "dev": true 722 | }, 723 | "generate-object-property": { 724 | "version": "1.2.0", 725 | "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz", 726 | "integrity": "sha1-nA4cQDCM6AT0eDYYuTf6iPmdUNA=", 727 | "dev": true, 728 | "requires": { 729 | "is-property": "^1.0.0" 730 | } 731 | }, 732 | "glob": { 733 | "version": "7.1.2", 734 | "resolved": "https://registry.npmjs.org/glob/-/glob-7.1.2.tgz", 735 | "integrity": "sha512-MJTUg1kjuLeQCJ+ccE4Vpa6kKVXkPYJ2mOCQyUuKLcLQsdrMCpBPUi8qVE6+YuaJkozeA9NusTAw3hLr8Xe5EQ==", 736 | "dev": true, 737 | "requires": { 738 | "fs.realpath": "^1.0.0", 739 | "inflight": "^1.0.4", 740 | "inherits": "2", 741 | "minimatch": "^3.0.4", 742 | "once": "^1.3.0", 743 | "path-is-absolute": "^1.0.0" 744 | } 745 | }, 746 | "globals": { 747 | "version": "9.18.0", 748 | "resolved": "https://registry.npmjs.org/globals/-/globals-9.18.0.tgz", 749 | "integrity": "sha512-S0nG3CLEQiY/ILxqtztTWH/3iRRdyBLw6KMDxnKMchrtbj2OFmehVh0WUCfW3DUrIgx/qFrJPICrq4Z4sTR9UQ==", 750 | "dev": true 751 | }, 752 | "globby": { 753 | "version": "5.0.0", 754 | "resolved": "https://registry.npmjs.org/globby/-/globby-5.0.0.tgz", 755 | "integrity": "sha1-69hGZ8oNuzMLmbz8aOrCvFQ3Dg0=", 756 | "dev": true, 757 | "requires": { 758 | "array-union": "^1.0.1", 759 | "arrify": "^1.0.0", 760 | "glob": "^7.0.3", 761 | "object-assign": "^4.0.1", 762 | "pify": "^2.0.0", 763 | "pinkie-promise": "^2.0.0" 764 | } 765 | }, 766 | "graceful-fs": { 767 | "version": "4.1.11", 768 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.1.11.tgz", 769 | "integrity": "sha1-Dovf5NHduIVNZOBOp8AOKgJuVlg=", 770 | "dev": true 771 | }, 772 | "growl": { 773 | "version": "1.9.2", 774 | "resolved": "https://registry.npmjs.org/growl/-/growl-1.9.2.tgz", 775 | "integrity": "sha1-Dqd0NxXbjY3ixe3hd14bRayFwC8=", 776 | "dev": true 777 | }, 778 | "has": { 779 | "version": "1.0.1", 780 | "resolved": "https://registry.npmjs.org/has/-/has-1.0.1.tgz", 781 | "integrity": "sha1-hGFzP1OLCDfJNh45qauelwTcLyg=", 782 | "dev": true, 783 | "requires": { 784 | "function-bind": "^1.0.2" 785 | } 786 | }, 787 | "has-ansi": { 788 | "version": "2.0.0", 789 | "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz", 790 | "integrity": "sha1-NPUEnOHs3ysGSa8+8k5F7TVBbZE=", 791 | "dev": true, 792 | "requires": { 793 | "ansi-regex": "^2.0.0" 794 | } 795 | }, 796 | "ignore": { 797 | "version": "3.3.3", 798 | "resolved": "https://registry.npmjs.org/ignore/-/ignore-3.3.3.tgz", 799 | "integrity": "sha1-QyNS5XrM2HqzEQ6C0/6g5HgSFW0=", 800 | "dev": true 801 | }, 802 | "imurmurhash": { 803 | "version": "0.1.4", 804 | "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", 805 | "integrity": "sha1-khi5srkoojixPcT7a21XbyMUU+o=", 806 | "dev": true 807 | }, 808 | "inflight": { 809 | "version": "1.0.6", 810 | "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", 811 | "integrity": "sha1-Sb1jMdfQLQwJvJEKEHW6gWW1bfk=", 812 | "dev": true, 813 | "requires": { 814 | "once": "^1.3.0", 815 | "wrappy": "1" 816 | } 817 | }, 818 | "inherits": { 819 | "version": "2.0.3", 820 | "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", 821 | "integrity": "sha1-Yzwsg+PaQqUC9SRmAiSA9CCCYd4=", 822 | "dev": true 823 | }, 824 | "inquirer": { 825 | "version": "0.12.0", 826 | "resolved": "https://registry.npmjs.org/inquirer/-/inquirer-0.12.0.tgz", 827 | "integrity": "sha1-HvK/1jUE3wvHV4X/+MLEHfEvB34=", 828 | "dev": true, 829 | "requires": { 830 | "ansi-escapes": "^1.1.0", 831 | "ansi-regex": "^2.0.0", 832 | "chalk": "^1.0.0", 833 | "cli-cursor": "^1.0.1", 834 | "cli-width": "^2.0.0", 835 | "figures": "^1.3.5", 836 | "lodash": "^4.3.0", 837 | "readline2": "^1.0.1", 838 | "run-async": "^0.1.0", 839 | "rx-lite": "^3.1.2", 840 | "string-width": "^1.0.1", 841 | "strip-ansi": "^3.0.0", 842 | "through": "^2.3.6" 843 | } 844 | }, 845 | "interpret": { 846 | "version": "1.0.3", 847 | "resolved": "https://registry.npmjs.org/interpret/-/interpret-1.0.3.tgz", 848 | "integrity": "sha1-y8NcYu7uc/Gat7EKgBURQBr8D5A=", 849 | "dev": true 850 | }, 851 | "is-callable": { 852 | "version": "1.1.3", 853 | "resolved": "https://registry.npmjs.org/is-callable/-/is-callable-1.1.3.tgz", 854 | "integrity": "sha1-hut1OSgF3cM69xySoO7fdO52BLI=", 855 | "dev": true 856 | }, 857 | "is-date-object": { 858 | "version": "1.0.1", 859 | "resolved": "https://registry.npmjs.org/is-date-object/-/is-date-object-1.0.1.tgz", 860 | "integrity": "sha1-mqIOtq7rv/d/vTPnTKAbM1gdOhY=", 861 | "dev": true 862 | }, 863 | "is-fullwidth-code-point": { 864 | "version": "1.0.0", 865 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-1.0.0.tgz", 866 | "integrity": "sha1-754xOG8DGn8NZDr4L95QxFfvAMs=", 867 | "dev": true, 868 | "requires": { 869 | "number-is-nan": "^1.0.0" 870 | } 871 | }, 872 | "is-my-json-valid": { 873 | "version": "2.16.0", 874 | "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.16.0.tgz", 875 | "integrity": "sha1-8Hndm/2uZe4gOKrorLyGqxCeNpM=", 876 | "dev": true, 877 | "requires": { 878 | "generate-function": "^2.0.0", 879 | "generate-object-property": "^1.1.0", 880 | "jsonpointer": "^4.0.0", 881 | "xtend": "^4.0.0" 882 | } 883 | }, 884 | "is-path-cwd": { 885 | "version": "1.0.0", 886 | "resolved": "https://registry.npmjs.org/is-path-cwd/-/is-path-cwd-1.0.0.tgz", 887 | "integrity": "sha1-0iXsIxMuie3Tj9p2dHLmLmXxEG0=", 888 | "dev": true 889 | }, 890 | "is-path-in-cwd": { 891 | "version": "1.0.0", 892 | "resolved": "https://registry.npmjs.org/is-path-in-cwd/-/is-path-in-cwd-1.0.0.tgz", 893 | "integrity": "sha1-ZHdYK4IU1gI0YJRWcAO+ip6sBNw=", 894 | "dev": true, 895 | "requires": { 896 | "is-path-inside": "^1.0.0" 897 | } 898 | }, 899 | "is-path-inside": { 900 | "version": "1.0.0", 901 | "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-1.0.0.tgz", 902 | "integrity": "sha1-/AbloWg/vaE95mev9xe7wQpI838=", 903 | "dev": true, 904 | "requires": { 905 | "path-is-inside": "^1.0.1" 906 | } 907 | }, 908 | "is-property": { 909 | "version": "1.0.2", 910 | "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz", 911 | "integrity": "sha1-V/4cTkhHTt1lsJkR8msc1Ald2oQ=", 912 | "dev": true 913 | }, 914 | "is-regex": { 915 | "version": "1.0.4", 916 | "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.0.4.tgz", 917 | "integrity": "sha1-VRdIm1RwkbCTDglWVM7SXul+lJE=", 918 | "dev": true, 919 | "requires": { 920 | "has": "^1.0.1" 921 | } 922 | }, 923 | "is-resolvable": { 924 | "version": "1.0.0", 925 | "resolved": "https://registry.npmjs.org/is-resolvable/-/is-resolvable-1.0.0.tgz", 926 | "integrity": "sha1-jfV8YeouPFAUCNEA+wE8+NbgzGI=", 927 | "dev": true, 928 | "requires": { 929 | "tryit": "^1.0.1" 930 | } 931 | }, 932 | "is-symbol": { 933 | "version": "1.0.1", 934 | "resolved": "https://registry.npmjs.org/is-symbol/-/is-symbol-1.0.1.tgz", 935 | "integrity": "sha1-PMWfAAJRlLarLjjbrmaJJWtmBXI=", 936 | "dev": true 937 | }, 938 | "isarray": { 939 | "version": "1.0.0", 940 | "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", 941 | "integrity": "sha1-u5NdSFgsuhaMBoNJV6VKPgcSTxE=", 942 | "dev": true 943 | }, 944 | "jade": { 945 | "version": "0.26.3", 946 | "resolved": "https://registry.npmjs.org/jade/-/jade-0.26.3.tgz", 947 | "integrity": "sha1-jxDXl32NefL2/4YqgbBRPMslaGw=", 948 | "dev": true, 949 | "requires": { 950 | "commander": "0.6.1", 951 | "mkdirp": "0.3.0" 952 | }, 953 | "dependencies": { 954 | "commander": { 955 | "version": "0.6.1", 956 | "resolved": "https://registry.npmjs.org/commander/-/commander-0.6.1.tgz", 957 | "integrity": "sha1-+mihT2qUXVTbvlDYzbMyDp47GgY=", 958 | "dev": true 959 | }, 960 | "mkdirp": { 961 | "version": "0.3.0", 962 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.3.0.tgz", 963 | "integrity": "sha1-G79asbqCevI1dRQ0kEJkVfSB/h4=", 964 | "dev": true 965 | } 966 | } 967 | }, 968 | "js-tokens": { 969 | "version": "3.0.2", 970 | "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-3.0.2.tgz", 971 | "integrity": "sha1-mGbfOVECEw449/mWvOtlRDIJwls=", 972 | "dev": true 973 | }, 974 | "js-yaml": { 975 | "version": "3.9.1", 976 | "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.9.1.tgz", 977 | "integrity": "sha512-CbcG379L1e+mWBnLvHWWeLs8GyV/EMw862uLI3c+GxVyDHWZcjZinwuBd3iW2pgxgIlksW/1vNJa4to+RvDOww==", 978 | "dev": true, 979 | "requires": { 980 | "argparse": "^1.0.7", 981 | "esprima": "^4.0.0" 982 | } 983 | }, 984 | "json-stable-stringify": { 985 | "version": "1.0.1", 986 | "resolved": "https://registry.npmjs.org/json-stable-stringify/-/json-stable-stringify-1.0.1.tgz", 987 | "integrity": "sha1-mnWdOcXy/1A/1TAGRu1EX4jE+a8=", 988 | "dev": true, 989 | "requires": { 990 | "jsonify": "~0.0.0" 991 | } 992 | }, 993 | "jsonify": { 994 | "version": "0.0.0", 995 | "resolved": "https://registry.npmjs.org/jsonify/-/jsonify-0.0.0.tgz", 996 | "integrity": "sha1-LHS27kHZPKUbe1qu6PUDYx0lKnM=", 997 | "dev": true 998 | }, 999 | "jsonpointer": { 1000 | "version": "4.0.1", 1001 | "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.1.tgz", 1002 | "integrity": "sha1-T9kss04OnbPInIYi7PUfm5eMbLk=", 1003 | "dev": true 1004 | }, 1005 | "jsx-ast-utils": { 1006 | "version": "1.4.1", 1007 | "resolved": "https://registry.npmjs.org/jsx-ast-utils/-/jsx-ast-utils-1.4.1.tgz", 1008 | "integrity": "sha1-OGchPo3Xm/Ho8jAMDPwe+xgsDfE=", 1009 | "dev": true 1010 | }, 1011 | "levn": { 1012 | "version": "0.3.0", 1013 | "resolved": "https://registry.npmjs.org/levn/-/levn-0.3.0.tgz", 1014 | "integrity": "sha1-OwmSTt+fCDwEkP3UwLxEIeBHZO4=", 1015 | "dev": true, 1016 | "requires": { 1017 | "prelude-ls": "~1.1.2", 1018 | "type-check": "~0.3.2" 1019 | } 1020 | }, 1021 | "lodash": { 1022 | "version": "4.17.11", 1023 | "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.11.tgz", 1024 | "integrity": "sha512-cQKh8igo5QUhZ7lg38DYWAxMvjSAKG0A8wGSVimP07SIUEK2UO+arSRKbRZWtelMtN5V0Hkwh5ryOto/SshYIg==" 1025 | }, 1026 | "lodash.cond": { 1027 | "version": "4.5.2", 1028 | "resolved": "https://registry.npmjs.org/lodash.cond/-/lodash.cond-4.5.2.tgz", 1029 | "integrity": "sha1-9HGh2khr5g9quVXRcRVSPdHSVdU=", 1030 | "dev": true 1031 | }, 1032 | "lodash.endswith": { 1033 | "version": "4.2.1", 1034 | "resolved": "https://registry.npmjs.org/lodash.endswith/-/lodash.endswith-4.2.1.tgz", 1035 | "integrity": "sha1-/tWawXOO0+I27dcGTsRWRIs3vAk=", 1036 | "dev": true 1037 | }, 1038 | "lodash.find": { 1039 | "version": "4.6.0", 1040 | "resolved": "https://registry.npmjs.org/lodash.find/-/lodash.find-4.6.0.tgz", 1041 | "integrity": "sha1-ywcE1Hq3F4n/oN6Ll92Sb7iLE7E=", 1042 | "dev": true 1043 | }, 1044 | "lodash.findindex": { 1045 | "version": "4.6.0", 1046 | "resolved": "https://registry.npmjs.org/lodash.findindex/-/lodash.findindex-4.6.0.tgz", 1047 | "integrity": "sha1-oyRd7mH7m24GJLU1ElYku2nBEQY=", 1048 | "dev": true 1049 | }, 1050 | "lru-cache": { 1051 | "version": "2.7.3", 1052 | "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-2.7.3.tgz", 1053 | "integrity": "sha1-bUUk6LlV+V1PW1iFHOId1y+06VI=", 1054 | "dev": true 1055 | }, 1056 | "minimatch": { 1057 | "version": "3.0.4", 1058 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.4.tgz", 1059 | "integrity": "sha512-yJHVQEhyqPLUTgt9B83PXu6W3rx4MvvHvSUvToogpwoGDOUQ+yDrR0HRot+yOCdCO7u4hX3pWft6kWBBcqh0UA==", 1060 | "dev": true, 1061 | "requires": { 1062 | "brace-expansion": "^1.1.7" 1063 | } 1064 | }, 1065 | "minimist": { 1066 | "version": "0.0.8", 1067 | "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", 1068 | "integrity": "sha1-hX/Kv8M5fSYluCKCYuhqp6ARsF0=", 1069 | "dev": true 1070 | }, 1071 | "mkdirp": { 1072 | "version": "0.5.1", 1073 | "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", 1074 | "integrity": "sha1-MAV0OOrGz3+MR2fzhkjWaX11yQM=", 1075 | "dev": true, 1076 | "requires": { 1077 | "minimist": "0.0.8" 1078 | } 1079 | }, 1080 | "mocha": { 1081 | "version": "2.5.3", 1082 | "resolved": "https://registry.npmjs.org/mocha/-/mocha-2.5.3.tgz", 1083 | "integrity": "sha1-FhvlvetJZ3HrmzV0UFC2IrWu/Fg=", 1084 | "dev": true, 1085 | "requires": { 1086 | "commander": "2.3.0", 1087 | "debug": "2.2.0", 1088 | "diff": "1.4.0", 1089 | "escape-string-regexp": "1.0.2", 1090 | "glob": "3.2.11", 1091 | "growl": "1.9.2", 1092 | "jade": "0.26.3", 1093 | "mkdirp": "0.5.1", 1094 | "supports-color": "1.2.0", 1095 | "to-iso-string": "0.0.2" 1096 | }, 1097 | "dependencies": { 1098 | "debug": { 1099 | "version": "2.2.0", 1100 | "resolved": "https://registry.npmjs.org/debug/-/debug-2.2.0.tgz", 1101 | "integrity": "sha1-+HBX6ZWxofauaklgZkE3vFbwOdo=", 1102 | "dev": true, 1103 | "requires": { 1104 | "ms": "0.7.1" 1105 | } 1106 | }, 1107 | "escape-string-regexp": { 1108 | "version": "1.0.2", 1109 | "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.2.tgz", 1110 | "integrity": "sha1-Tbwv5nTnGUnK8/smlc5/LcHZqNE=", 1111 | "dev": true 1112 | }, 1113 | "glob": { 1114 | "version": "3.2.11", 1115 | "resolved": "https://registry.npmjs.org/glob/-/glob-3.2.11.tgz", 1116 | "integrity": "sha1-Spc/Y1uRkPcV0QmH1cAP0oFevj0=", 1117 | "dev": true, 1118 | "requires": { 1119 | "inherits": "2", 1120 | "minimatch": "0.3" 1121 | } 1122 | }, 1123 | "minimatch": { 1124 | "version": "0.3.0", 1125 | "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-0.3.0.tgz", 1126 | "integrity": "sha1-J12O2qxPG7MyZHIInnlJyDlGmd0=", 1127 | "dev": true, 1128 | "requires": { 1129 | "lru-cache": "2", 1130 | "sigmund": "~1.0.0" 1131 | } 1132 | }, 1133 | "ms": { 1134 | "version": "0.7.1", 1135 | "resolved": "https://registry.npmjs.org/ms/-/ms-0.7.1.tgz", 1136 | "integrity": "sha1-nNE8A62/8ltl7/3nzoZO6VIBcJg=", 1137 | "dev": true 1138 | }, 1139 | "supports-color": { 1140 | "version": "1.2.0", 1141 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-1.2.0.tgz", 1142 | "integrity": "sha1-/x7R5hFp0Gs88tWI4YixjYhH4X4=", 1143 | "dev": true 1144 | } 1145 | } 1146 | }, 1147 | "ms": { 1148 | "version": "2.0.0", 1149 | "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", 1150 | "integrity": "sha1-VgiurfwAvmwpAd9fmGF4jeDVl8g=", 1151 | "dev": true 1152 | }, 1153 | "mute-stream": { 1154 | "version": "0.0.5", 1155 | "resolved": "https://registry.npmjs.org/mute-stream/-/mute-stream-0.0.5.tgz", 1156 | "integrity": "sha1-j7+rsKmKJT0xhDMfno3rc3L6xsA=", 1157 | "dev": true 1158 | }, 1159 | "natural-compare": { 1160 | "version": "1.4.0", 1161 | "resolved": "https://registry.npmjs.org/natural-compare/-/natural-compare-1.4.0.tgz", 1162 | "integrity": "sha1-Sr6/7tdUHywnrPspvbvRXI1bpPc=", 1163 | "dev": true 1164 | }, 1165 | "number-is-nan": { 1166 | "version": "1.0.1", 1167 | "resolved": "https://registry.npmjs.org/number-is-nan/-/number-is-nan-1.0.1.tgz", 1168 | "integrity": "sha1-CXtgK1NCKlIsGvuHkDGDNpQaAR0=", 1169 | "dev": true 1170 | }, 1171 | "object-assign": { 1172 | "version": "4.1.1", 1173 | "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", 1174 | "integrity": "sha1-IQmtx5ZYh8/AXLvUQsrIv7s2CGM=", 1175 | "dev": true 1176 | }, 1177 | "object-keys": { 1178 | "version": "1.0.11", 1179 | "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.0.11.tgz", 1180 | "integrity": "sha1-xUYBd4rVYPEULODgG8yotW0TQm0=", 1181 | "dev": true 1182 | }, 1183 | "object.assign": { 1184 | "version": "4.0.4", 1185 | "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.0.4.tgz", 1186 | "integrity": "sha1-scnMBE7xuf5jYG/BQau7MuFHMMw=", 1187 | "dev": true, 1188 | "requires": { 1189 | "define-properties": "^1.1.2", 1190 | "function-bind": "^1.1.0", 1191 | "object-keys": "^1.0.10" 1192 | } 1193 | }, 1194 | "once": { 1195 | "version": "1.4.0", 1196 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 1197 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 1198 | "dev": true, 1199 | "requires": { 1200 | "wrappy": "1" 1201 | } 1202 | }, 1203 | "onetime": { 1204 | "version": "1.1.0", 1205 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-1.1.0.tgz", 1206 | "integrity": "sha1-ofeDj4MUxRbwXs78vEzP4EtO14k=", 1207 | "dev": true 1208 | }, 1209 | "optionator": { 1210 | "version": "0.8.2", 1211 | "resolved": "https://registry.npmjs.org/optionator/-/optionator-0.8.2.tgz", 1212 | "integrity": "sha1-NkxeQJ0/TWMB1sC0wFu6UBgK62Q=", 1213 | "dev": true, 1214 | "requires": { 1215 | "deep-is": "~0.1.3", 1216 | "fast-levenshtein": "~2.0.4", 1217 | "levn": "~0.3.0", 1218 | "prelude-ls": "~1.1.2", 1219 | "type-check": "~0.3.2", 1220 | "wordwrap": "~1.0.0" 1221 | } 1222 | }, 1223 | "os-homedir": { 1224 | "version": "1.0.2", 1225 | "resolved": "https://registry.npmjs.org/os-homedir/-/os-homedir-1.0.2.tgz", 1226 | "integrity": "sha1-/7xJiDNuDoM94MFox+8VISGqf7M=", 1227 | "dev": true 1228 | }, 1229 | "parse": { 1230 | "version": "2.11.0", 1231 | "resolved": "https://registry.npmjs.org/parse/-/parse-2.11.0.tgz", 1232 | "integrity": "sha512-dbGdA5M1ylky4T/b5pOXeYhsHwzATz/JbweCiBtdJLsnb8SylSSgA7V0U96RtXBI1Hfzp5uFZpqmnUKr5t69NA==", 1233 | "dev": true, 1234 | "requires": { 1235 | "@babel/runtime": "7.7.7", 1236 | "@babel/runtime-corejs3": "7.7.7", 1237 | "crypto-js": "3.1.9-1", 1238 | "uuid": "3.3.3", 1239 | "ws": "7.2.1", 1240 | "xmlhttprequest": "1.8.0" 1241 | } 1242 | }, 1243 | "path-exists": { 1244 | "version": "2.1.0", 1245 | "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-2.1.0.tgz", 1246 | "integrity": "sha1-D+tsZPD8UY2adU3V77YscCJ2H0s=", 1247 | "dev": true, 1248 | "requires": { 1249 | "pinkie-promise": "^2.0.0" 1250 | } 1251 | }, 1252 | "path-is-absolute": { 1253 | "version": "1.0.1", 1254 | "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", 1255 | "integrity": "sha1-F0uSaHNVNP+8es5r9TpanhtcX18=", 1256 | "dev": true 1257 | }, 1258 | "path-is-inside": { 1259 | "version": "1.0.2", 1260 | "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", 1261 | "integrity": "sha1-NlQX3t5EQw0cEa9hAn+s8HS9/FM=", 1262 | "dev": true 1263 | }, 1264 | "path-parse": { 1265 | "version": "1.0.5", 1266 | "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.5.tgz", 1267 | "integrity": "sha1-PBrfhx6pzWyUMbbqK9dKD/BVxME=", 1268 | "dev": true 1269 | }, 1270 | "pify": { 1271 | "version": "2.3.0", 1272 | "resolved": "https://registry.npmjs.org/pify/-/pify-2.3.0.tgz", 1273 | "integrity": "sha1-7RQaasBDqEnqWISY59yosVMw6Qw=", 1274 | "dev": true 1275 | }, 1276 | "pinkie": { 1277 | "version": "2.0.4", 1278 | "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz", 1279 | "integrity": "sha1-clVrgM+g1IqXToDnckjoDtT3+HA=", 1280 | "dev": true 1281 | }, 1282 | "pinkie-promise": { 1283 | "version": "2.0.1", 1284 | "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz", 1285 | "integrity": "sha1-ITXW36ejWMBprJsXh3YogihFD/o=", 1286 | "dev": true, 1287 | "requires": { 1288 | "pinkie": "^2.0.0" 1289 | } 1290 | }, 1291 | "pkg-dir": { 1292 | "version": "1.0.0", 1293 | "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-1.0.0.tgz", 1294 | "integrity": "sha1-ektQio1bstYp1EcFb/TpyTFM89Q=", 1295 | "dev": true, 1296 | "requires": { 1297 | "find-up": "^1.0.0" 1298 | } 1299 | }, 1300 | "pkg-up": { 1301 | "version": "1.0.0", 1302 | "resolved": "https://registry.npmjs.org/pkg-up/-/pkg-up-1.0.0.tgz", 1303 | "integrity": "sha1-Pgj7RhUlxEIWJKM7n35tCvWwWiY=", 1304 | "dev": true, 1305 | "requires": { 1306 | "find-up": "^1.0.0" 1307 | } 1308 | }, 1309 | "pluralize": { 1310 | "version": "1.2.1", 1311 | "resolved": "https://registry.npmjs.org/pluralize/-/pluralize-1.2.1.tgz", 1312 | "integrity": "sha1-0aIUg/0iu0HlihL6NCGCMUCJfEU=", 1313 | "dev": true 1314 | }, 1315 | "prelude-ls": { 1316 | "version": "1.1.2", 1317 | "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.1.2.tgz", 1318 | "integrity": "sha1-IZMqVJ9eUv/ZqCf1cOBL5iqX2lQ=", 1319 | "dev": true 1320 | }, 1321 | "process-nextick-args": { 1322 | "version": "1.0.7", 1323 | "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-1.0.7.tgz", 1324 | "integrity": "sha1-FQ4gt1ZZCtP5EJPyWk8q2L/zC6M=", 1325 | "dev": true 1326 | }, 1327 | "progress": { 1328 | "version": "1.1.8", 1329 | "resolved": "https://registry.npmjs.org/progress/-/progress-1.1.8.tgz", 1330 | "integrity": "sha1-4mDHj2Fhzdmw5WzD4Khd4Xx6V74=", 1331 | "dev": true 1332 | }, 1333 | "readable-stream": { 1334 | "version": "2.3.3", 1335 | "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.3.tgz", 1336 | "integrity": "sha512-m+qzzcn7KUxEmd1gMbchF+Y2eIUbieUaxkWtptyHywrX0rE8QEYqPC07Vuy4Wm32/xE16NcdBctb8S0Xe/5IeQ==", 1337 | "dev": true, 1338 | "requires": { 1339 | "core-util-is": "~1.0.0", 1340 | "inherits": "~2.0.3", 1341 | "isarray": "~1.0.0", 1342 | "process-nextick-args": "~1.0.6", 1343 | "safe-buffer": "~5.1.1", 1344 | "string_decoder": "~1.0.3", 1345 | "util-deprecate": "~1.0.1" 1346 | }, 1347 | "dependencies": { 1348 | "safe-buffer": { 1349 | "version": "5.1.1", 1350 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1351 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", 1352 | "dev": true 1353 | } 1354 | } 1355 | }, 1356 | "readline2": { 1357 | "version": "1.0.1", 1358 | "resolved": "https://registry.npmjs.org/readline2/-/readline2-1.0.1.tgz", 1359 | "integrity": "sha1-QQWWCP/BVHV7cV2ZidGZ/783LjU=", 1360 | "dev": true, 1361 | "requires": { 1362 | "code-point-at": "^1.0.0", 1363 | "is-fullwidth-code-point": "^1.0.0", 1364 | "mute-stream": "0.0.5" 1365 | } 1366 | }, 1367 | "rechoir": { 1368 | "version": "0.6.2", 1369 | "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.6.2.tgz", 1370 | "integrity": "sha1-hSBLVNuoLVdC4oyWdW70OvUOM4Q=", 1371 | "dev": true, 1372 | "requires": { 1373 | "resolve": "^1.1.6" 1374 | } 1375 | }, 1376 | "regenerator-runtime": { 1377 | "version": "0.13.3", 1378 | "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.13.3.tgz", 1379 | "integrity": "sha512-naKIZz2GQ8JWh///G7L3X6LaQUAMp2lvb1rvwwsURe/VXwD6VMfr+/1NuNw3ag8v2kY1aQ/go5SNn79O9JU7yw==", 1380 | "dev": true 1381 | }, 1382 | "require-uncached": { 1383 | "version": "1.0.3", 1384 | "resolved": "https://registry.npmjs.org/require-uncached/-/require-uncached-1.0.3.tgz", 1385 | "integrity": "sha1-Tg1W1slmL9MeQwEcS5WqSZVUIdM=", 1386 | "dev": true, 1387 | "requires": { 1388 | "caller-path": "^0.1.0", 1389 | "resolve-from": "^1.0.0" 1390 | } 1391 | }, 1392 | "resolve": { 1393 | "version": "1.4.0", 1394 | "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.4.0.tgz", 1395 | "integrity": "sha512-aW7sVKPufyHqOmyyLzg/J+8606v5nevBgaliIlV7nUpVMsDnoBGV/cbSLNjZAg9q0Cfd/+easKVKQ8vOu8fn1Q==", 1396 | "dev": true, 1397 | "requires": { 1398 | "path-parse": "^1.0.5" 1399 | } 1400 | }, 1401 | "resolve-from": { 1402 | "version": "1.0.1", 1403 | "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-1.0.1.tgz", 1404 | "integrity": "sha1-Jsv+k10a7uq7Kbw/5a6wHpPUQiY=", 1405 | "dev": true 1406 | }, 1407 | "restore-cursor": { 1408 | "version": "1.0.1", 1409 | "resolved": "https://registry.npmjs.org/restore-cursor/-/restore-cursor-1.0.1.tgz", 1410 | "integrity": "sha1-NGYfRohjJ/7SmRR5FSJS35LapUE=", 1411 | "dev": true, 1412 | "requires": { 1413 | "exit-hook": "^1.0.0", 1414 | "onetime": "^1.0.0" 1415 | } 1416 | }, 1417 | "rimraf": { 1418 | "version": "2.6.1", 1419 | "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.6.1.tgz", 1420 | "integrity": "sha1-wjOOxkPfeht/5cVPqG9XQopV8z0=", 1421 | "dev": true, 1422 | "requires": { 1423 | "glob": "^7.0.5" 1424 | } 1425 | }, 1426 | "run-async": { 1427 | "version": "0.1.0", 1428 | "resolved": "https://registry.npmjs.org/run-async/-/run-async-0.1.0.tgz", 1429 | "integrity": "sha1-yK1KXhEGYeQCp9IbUw4AnyX444k=", 1430 | "dev": true, 1431 | "requires": { 1432 | "once": "^1.3.0" 1433 | } 1434 | }, 1435 | "rx-lite": { 1436 | "version": "3.1.2", 1437 | "resolved": "https://registry.npmjs.org/rx-lite/-/rx-lite-3.1.2.tgz", 1438 | "integrity": "sha1-Gc5QLKVyZl87ZHsQk5+X/RYV8QI=", 1439 | "dev": true 1440 | }, 1441 | "shelljs": { 1442 | "version": "0.7.8", 1443 | "resolved": "https://registry.npmjs.org/shelljs/-/shelljs-0.7.8.tgz", 1444 | "integrity": "sha1-3svPh0sNHl+3LhSxZKloMEjprLM=", 1445 | "dev": true, 1446 | "requires": { 1447 | "glob": "^7.0.0", 1448 | "interpret": "^1.0.0", 1449 | "rechoir": "^0.6.2" 1450 | } 1451 | }, 1452 | "sigmund": { 1453 | "version": "1.0.1", 1454 | "resolved": "https://registry.npmjs.org/sigmund/-/sigmund-1.0.1.tgz", 1455 | "integrity": "sha1-P/IfGYytIXX587eBhT/ZTQ0ZtZA=", 1456 | "dev": true 1457 | }, 1458 | "slice-ansi": { 1459 | "version": "0.0.4", 1460 | "resolved": "https://registry.npmjs.org/slice-ansi/-/slice-ansi-0.0.4.tgz", 1461 | "integrity": "sha1-7b+JA/ZvfOL46v1s7tZeJkyDGzU=", 1462 | "dev": true 1463 | }, 1464 | "sprintf-js": { 1465 | "version": "1.0.3", 1466 | "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", 1467 | "integrity": "sha1-BOaSb2YolTVPPdAVIDYzuFcpfiw=", 1468 | "dev": true 1469 | }, 1470 | "string-width": { 1471 | "version": "1.0.2", 1472 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-1.0.2.tgz", 1473 | "integrity": "sha1-EYvfW4zcUaKn5w0hHgfisLmxB9M=", 1474 | "dev": true, 1475 | "requires": { 1476 | "code-point-at": "^1.0.0", 1477 | "is-fullwidth-code-point": "^1.0.0", 1478 | "strip-ansi": "^3.0.0" 1479 | } 1480 | }, 1481 | "string_decoder": { 1482 | "version": "1.0.3", 1483 | "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.0.3.tgz", 1484 | "integrity": "sha512-4AH6Z5fzNNBcH+6XDMfA/BTt87skxqJlO0lAh3Dker5zThcAxG6mKz+iGu308UKoPPQ8Dcqx/4JhujzltRa+hQ==", 1485 | "dev": true, 1486 | "requires": { 1487 | "safe-buffer": "~5.1.0" 1488 | }, 1489 | "dependencies": { 1490 | "safe-buffer": { 1491 | "version": "5.1.1", 1492 | "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.1.tgz", 1493 | "integrity": "sha512-kKvNJn6Mm93gAczWVJg7wH+wGYWNrDHdWvpUmHyEsgCtIwwo3bqPtV4tR5tuPaUhTOo/kvhVwd8XwwOllGYkbg==", 1494 | "dev": true 1495 | } 1496 | } 1497 | }, 1498 | "strip-ansi": { 1499 | "version": "3.0.1", 1500 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz", 1501 | "integrity": "sha1-ajhfuIU9lS1f8F0Oiq+UJ43GPc8=", 1502 | "dev": true, 1503 | "requires": { 1504 | "ansi-regex": "^2.0.0" 1505 | } 1506 | }, 1507 | "strip-bom": { 1508 | "version": "3.0.0", 1509 | "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", 1510 | "integrity": "sha1-IzTBjpx1n3vdVv3vfprj1YjmjtM=", 1511 | "dev": true 1512 | }, 1513 | "strip-json-comments": { 1514 | "version": "2.0.1", 1515 | "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", 1516 | "integrity": "sha1-PFMZQukIwml8DsNEhYwobHygpgo=", 1517 | "dev": true 1518 | }, 1519 | "supports-color": { 1520 | "version": "2.0.0", 1521 | "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz", 1522 | "integrity": "sha1-U10EXOa2Nj+kARcIRimZXp3zJMc=", 1523 | "dev": true 1524 | }, 1525 | "table": { 1526 | "version": "3.8.3", 1527 | "resolved": "https://registry.npmjs.org/table/-/table-3.8.3.tgz", 1528 | "integrity": "sha1-K7xULw/amGGnVdOUf+/Ys/UThV8=", 1529 | "dev": true, 1530 | "requires": { 1531 | "ajv": "^4.7.0", 1532 | "ajv-keywords": "^1.0.0", 1533 | "chalk": "^1.1.1", 1534 | "lodash": "^4.0.0", 1535 | "slice-ansi": "0.0.4", 1536 | "string-width": "^2.0.0" 1537 | }, 1538 | "dependencies": { 1539 | "ansi-regex": { 1540 | "version": "3.0.0", 1541 | "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-3.0.0.tgz", 1542 | "integrity": "sha1-7QMXwyIGT3lGbAKWa922Bas32Zg=", 1543 | "dev": true 1544 | }, 1545 | "is-fullwidth-code-point": { 1546 | "version": "2.0.0", 1547 | "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-2.0.0.tgz", 1548 | "integrity": "sha1-o7MKXE8ZkYMWeqq5O+764937ZU8=", 1549 | "dev": true 1550 | }, 1551 | "string-width": { 1552 | "version": "2.1.1", 1553 | "resolved": "https://registry.npmjs.org/string-width/-/string-width-2.1.1.tgz", 1554 | "integrity": "sha512-nOqH59deCq9SRHlxq1Aw85Jnt4w6KvLKqWVik6oA9ZklXLNIOlqg4F2yrT1MVaTjAqvVwdfeZ7w7aCvJD7ugkw==", 1555 | "dev": true, 1556 | "requires": { 1557 | "is-fullwidth-code-point": "^2.0.0", 1558 | "strip-ansi": "^4.0.0" 1559 | } 1560 | }, 1561 | "strip-ansi": { 1562 | "version": "4.0.0", 1563 | "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-4.0.0.tgz", 1564 | "integrity": "sha1-qEeQIusaw2iocTibY1JixQXuNo8=", 1565 | "dev": true, 1566 | "requires": { 1567 | "ansi-regex": "^3.0.0" 1568 | } 1569 | } 1570 | } 1571 | }, 1572 | "text-table": { 1573 | "version": "0.2.0", 1574 | "resolved": "https://registry.npmjs.org/text-table/-/text-table-0.2.0.tgz", 1575 | "integrity": "sha1-f17oI66AUgfACvLfSoTsP8+lcLQ=", 1576 | "dev": true 1577 | }, 1578 | "through": { 1579 | "version": "2.3.8", 1580 | "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", 1581 | "integrity": "sha1-DdTJ/6q8NXlgsbckEV1+Doai4fU=", 1582 | "dev": true 1583 | }, 1584 | "to-iso-string": { 1585 | "version": "0.0.2", 1586 | "resolved": "https://registry.npmjs.org/to-iso-string/-/to-iso-string-0.0.2.tgz", 1587 | "integrity": "sha1-TcGeZk38y+Jb2NtQiwDG2hWCVdE=", 1588 | "dev": true 1589 | }, 1590 | "tryit": { 1591 | "version": "1.0.3", 1592 | "resolved": "https://registry.npmjs.org/tryit/-/tryit-1.0.3.tgz", 1593 | "integrity": "sha1-OTvnMKlEb9Hq1tpZoBQwjzbCics=", 1594 | "dev": true 1595 | }, 1596 | "type-check": { 1597 | "version": "0.3.2", 1598 | "resolved": "https://registry.npmjs.org/type-check/-/type-check-0.3.2.tgz", 1599 | "integrity": "sha1-WITKtRLPHTVeP7eE8wgEsrUg23I=", 1600 | "dev": true, 1601 | "requires": { 1602 | "prelude-ls": "~1.1.2" 1603 | } 1604 | }, 1605 | "typedarray": { 1606 | "version": "0.0.6", 1607 | "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz", 1608 | "integrity": "sha1-hnrHTjhkGHsdPUfZlqeOxciDB3c=", 1609 | "dev": true 1610 | }, 1611 | "user-home": { 1612 | "version": "2.0.0", 1613 | "resolved": "https://registry.npmjs.org/user-home/-/user-home-2.0.0.tgz", 1614 | "integrity": "sha1-nHC/2Babwdy/SGBODwS4tJzenp8=", 1615 | "dev": true, 1616 | "requires": { 1617 | "os-homedir": "^1.0.0" 1618 | } 1619 | }, 1620 | "util-deprecate": { 1621 | "version": "1.0.2", 1622 | "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", 1623 | "integrity": "sha1-RQ1Nyfpw3nMnYvvS1KKJgUGaDM8=", 1624 | "dev": true 1625 | }, 1626 | "uuid": { 1627 | "version": "3.3.3", 1628 | "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.3.3.tgz", 1629 | "integrity": "sha512-pW0No1RGHgzlpHJO1nsVrHKpOEIxkGg1xB+v0ZmdNH5OAeAwzAVrCnI2/6Mtx+Uys6iaylxa+D3g4j63IKKjSQ==", 1630 | "dev": true 1631 | }, 1632 | "wordwrap": { 1633 | "version": "1.0.0", 1634 | "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-1.0.0.tgz", 1635 | "integrity": "sha1-J1hIEIkUVqQXHI0CJkQa3pDLyus=", 1636 | "dev": true 1637 | }, 1638 | "wrappy": { 1639 | "version": "1.0.2", 1640 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 1641 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=", 1642 | "dev": true 1643 | }, 1644 | "write": { 1645 | "version": "0.2.1", 1646 | "resolved": "https://registry.npmjs.org/write/-/write-0.2.1.tgz", 1647 | "integrity": "sha1-X8A4KOJkzqP+kUVUdvejxWbLB1c=", 1648 | "dev": true, 1649 | "requires": { 1650 | "mkdirp": "^0.5.1" 1651 | } 1652 | }, 1653 | "ws": { 1654 | "version": "7.2.1", 1655 | "resolved": "https://registry.npmjs.org/ws/-/ws-7.2.1.tgz", 1656 | "integrity": "sha512-sucePNSafamSKoOqoNfBd8V0StlkzJKL2ZAhGQinCfNQ+oacw+Pk7lcdAElecBF2VkLNZRiIb5Oi1Q5lVUVt2A==", 1657 | "dev": true 1658 | }, 1659 | "xmlhttprequest": { 1660 | "version": "1.8.0", 1661 | "resolved": "https://registry.npmjs.org/xmlhttprequest/-/xmlhttprequest-1.8.0.tgz", 1662 | "integrity": "sha1-Z/4HXFwk/vOfnWX197f+dRcZaPw=", 1663 | "dev": true 1664 | }, 1665 | "xtend": { 1666 | "version": "4.0.1", 1667 | "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz", 1668 | "integrity": "sha1-pcbVMr5lbiPbgg77lDofBJmNY68=", 1669 | "dev": true 1670 | } 1671 | } 1672 | } 1673 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-mockdb", 3 | "version": "0.4.0", 4 | "description": "Parse JS SDK Mocked Database", 5 | "main": "src/parse-mockdb.js", 6 | "engines": { 7 | "node": ">=5.0.0" 8 | }, 9 | "scripts": { 10 | "test": "npm run lint && mocha ./test/test", 11 | "lint": "eslint src/** test/**" 12 | }, 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Hustle/parse-mockdb.git" 16 | }, 17 | "keywords": [ 18 | "parse", 19 | "parsesdk", 20 | "mock", 21 | "unit-testing" 22 | ], 23 | "author": "Tyler Brock, Roddy Lindsay", 24 | "license": "MIT", 25 | "bugs": { 26 | "url": "https://github.com/Hustle/parse-mockdb/issues" 27 | }, 28 | "homepage": "https://github.com/Hustle/parse-mockdb", 29 | "dependencies": { 30 | "lodash": "^4.17.11" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^3.4.0", 34 | "eslint-config-airbnb": "^10.0.1", 35 | "eslint-plugin-import": "^1.14.0", 36 | "eslint-plugin-jsx-a11y": "^2.2.0", 37 | "eslint-plugin-react": "^6.2.0", 38 | "mocha": "^2.2.5", 39 | "parse": "^2.0.0" 40 | }, 41 | "directories": { 42 | "test": "test" 43 | }, 44 | "eslintConfig": { 45 | "extends": "airbnb", 46 | "parserOptions": { 47 | "ecmaVersion": 5 48 | }, 49 | "env": { 50 | "node": true, 51 | "mocha": true 52 | }, 53 | "rules": { 54 | "no-param-reassign": "off", 55 | "no-underscore-dangle": "off", 56 | "no-console": "off", 57 | "new-cap": "off", 58 | "strict": "off", 59 | "prefer-rest-params": "off" 60 | } 61 | }, 62 | "peerDependencies": { 63 | "parse": "^2.0.0" 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/crypto.js: -------------------------------------------------------------------------------- 1 | // Copied from src/cryptoUtils in parse-server package 2 | const crypto = require('crypto'); 3 | 4 | // Returns a new random hex string of the given even size. 5 | function randomHexString(size) { 6 | if (size === 0) { 7 | throw new Error('Zero-length randomHexString is useless.'); 8 | } 9 | if (size % 2 !== 0) { 10 | throw new Error('randomHexString size must be divisible by 2.'); 11 | } 12 | return crypto.randomBytes(size / 2).toString('hex'); 13 | } 14 | 15 | // Returns a new random alphanumeric string of the given size. 16 | // 17 | // Note: to simplify implementation, the result has slight modulo bias, 18 | // because chars length of 62 doesn't divide the number of all bytes 19 | // (256) evenly. Such bias is acceptable for most cases when the output 20 | // length is long enough and doesn't need to be uniform. 21 | function randomString(size) { 22 | if (size === 0) { 23 | throw new Error('Zero-length randomString is useless.'); 24 | } 25 | const chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' + 26 | 'abcdefghijklmnopqrstuvwxyz' + 27 | '0123456789'); 28 | let objectId = ''; 29 | const bytes = crypto.randomBytes(size); 30 | for (let i = 0; i < bytes.length; ++i) { 31 | objectId += chars[bytes.readUInt8(i) % chars.length]; 32 | } 33 | return objectId; 34 | } 35 | 36 | // Returns a new random alphanumeric string suitable for object ID. 37 | function newObjectId(size = 10) { 38 | return randomString(size); 39 | } 40 | 41 | // Returns a new random hex string suitable for secure tokens. 42 | function newToken() { 43 | return randomHexString(32); 44 | } 45 | 46 | function md5Hash(string) { 47 | return crypto.createHash('md5').update(string).digest('hex'); 48 | } 49 | 50 | module.exports = { 51 | randomHexString, 52 | randomString, 53 | newObjectId, 54 | newToken, 55 | md5Hash, 56 | }; 57 | -------------------------------------------------------------------------------- /src/parse-mockdb.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const _ = require('lodash'); 4 | 5 | const crypto = require('./crypto'); 6 | 7 | const DEFAULT_LIMIT = 100; 8 | const QUOTE_REGEXP = /(\\Q|\\E)/g; 9 | 10 | const CONFIG = { 11 | DEBUG: process.env.DEBUG_DB, 12 | }; 13 | 14 | let Parse; 15 | let db = {}; 16 | let hooks = {}; 17 | const masks = {}; 18 | 19 | let indirect = null; 20 | let outOfBandResults = null; 21 | 22 | let defaultController = null; 23 | let mocked = false; 24 | let user = null; 25 | 26 | function debugPrint(prefix, object) { 27 | if (CONFIG.DEBUG) { 28 | console.log(['[', ']'].join(prefix), JSON.stringify(object, null, 4)); 29 | } 30 | } 31 | 32 | function isOp(object) { 33 | return object && typeof object === 'object' && '__op' in object; 34 | } 35 | 36 | function isPointer(object) { 37 | return object && object.__type === 'Pointer'; 38 | } 39 | 40 | function isDate(object) { 41 | return object && object.__type === 'Date'; 42 | } 43 | 44 | /** 45 | * Deserialize an encoded query parameter if necessary 46 | */ 47 | function deserializeQueryParam(param) { 48 | if (!!param && (typeof param === 'object')) { 49 | if (param.__type === 'Date') { 50 | return new Date(param.iso); 51 | } 52 | } 53 | return param; 54 | } 55 | 56 | /** 57 | * Evaluates whether 2 objects are the same, independent of their representation 58 | * (e.g. Pointer, Object) 59 | */ 60 | function objectsAreEqual(obj1, obj2) { 61 | // Always search through array on array columns 62 | if (Array.isArray(obj1)) { 63 | if (Array.isArray(obj2)) { 64 | throw new Parse.Error(107, `You cannot use ${obj2} as a query parameter`); 65 | } else { 66 | return _.some(obj1, obj => objectsAreEqual(obj, obj2)); 67 | } 68 | } 69 | 70 | // scalar values (including null/undefined) 71 | // Note: undefined equals null. 72 | // For all other objects, strict equality is applied 73 | if (obj1 === obj2 || (_.isNil(obj1) && _.isNil(obj2))) { 74 | return true; 75 | } 76 | 77 | // if any of those is null or undefined the other is not because 78 | // of above --> abort 79 | if (_.isNil(obj1) || _.isNil(obj2)) { 80 | return false; 81 | } 82 | 83 | // objects 84 | if (_.isEqual(obj1, obj2)) { 85 | return true; 86 | } 87 | 88 | // both pointers 89 | if (obj1.objectId !== undefined && obj1.objectId === obj2.objectId) { 90 | return true; 91 | } 92 | 93 | // both dates 94 | if (isDate(obj1) && isDate(obj2)) { 95 | return deserializeQueryParam(obj1) === deserializeQueryParam(obj2); 96 | } 97 | 98 | return false; 99 | } 100 | 101 | 102 | // Ensures `object` has an array at `key`. Creates array if `key` doesn't exist. 103 | // Will throw if value for `key` exists and is not Array. 104 | function ensureArray(object, key) { 105 | if (!object[key]) { 106 | object[key] = []; 107 | } 108 | if (!Array.isArray(object[key])) { 109 | throw new Error("Can't perform array operation on non-array field"); 110 | } 111 | } 112 | 113 | const MASKED_UPDATE_OPS = new Set(['AddRelation', 'RemoveRelation']); 114 | 115 | /** 116 | * Update Operators. 117 | * 118 | * Params: 119 | * object - object on which to operate 120 | * key - value to be modified in bound object. 121 | * value - operator value, i.e. `{__op: "Increment", amount: 1}` 122 | */ 123 | const UPDATE_OPERATORS = { 124 | Increment: (object, key, value) => { 125 | if (object[key] === undefined) { 126 | object[key] = 0; 127 | } 128 | object[key] += value.amount; 129 | }, 130 | Add: (object, key, value) => { 131 | ensureArray(object, key); 132 | value.objects.forEach(el => { 133 | object[key].push(el); 134 | }); 135 | }, 136 | AddUnique: (object, key, value) => { 137 | ensureArray(object, key); 138 | const array = object[key]; 139 | value.objects.forEach(el => { 140 | if (!_.some(array, e => objectsAreEqual(e, el))) { 141 | array.push(el); 142 | } 143 | }); 144 | }, 145 | Remove: (object, key, value) => { 146 | ensureArray(object, key); 147 | const array = object[key]; 148 | value.objects.forEach(el => { 149 | _.remove(array, item => objectsAreEqual(item, el)); 150 | }); 151 | }, 152 | Delete: (object, key) => { 153 | delete object[key]; 154 | }, 155 | AddRelation: (object, key, value) => { 156 | ensureArray(object, key); 157 | const relation = object[key]; 158 | value.objects.forEach(pointer => { 159 | if (!_.some(relation, e => objectsAreEqual(e, pointer))) { 160 | relation.push(pointer); 161 | } 162 | }); 163 | }, 164 | RemoveRelation: (object, key, value) => { 165 | ensureArray(object, key); 166 | const relation = object[key]; 167 | value.objects.forEach(item => { 168 | _.remove(relation, pointer => objectsAreEqual(pointer, item)); 169 | }); 170 | }, 171 | }; 172 | 173 | function getCollection(collection) { 174 | if (!db[collection]) { 175 | db[collection] = {}; 176 | } 177 | return db[collection]; 178 | } 179 | 180 | function getMask(collection) { 181 | if (!masks[collection]) { 182 | masks[collection] = new Set(); 183 | } 184 | return masks[collection]; 185 | } 186 | 187 | /** 188 | * Clears the MockDB and any registered hooks. 189 | */ 190 | function cleanUp() { 191 | db = {}; 192 | hooks = {}; 193 | } 194 | 195 | /** 196 | * Registers a hook on a class denoted by className. 197 | * 198 | * @param {string} className The name of the class to register hook on. 199 | * @param {string} hookType One of 'beforeSave', 'afterSave', 'beforeDelete', 'afterDelete' 200 | * @param {function} hookFn Function that will be called with `this` bound to hydrated model. 201 | * Must return a promise. 202 | * 203 | * @note Only supports beforeSave, beforeDelete, and afterSave at the moment. 204 | */ 205 | function registerHook(className, hookType, hookFn) { 206 | if (!hooks[className]) { 207 | hooks[className] = {}; 208 | } 209 | 210 | hooks[className][hookType] = hookFn; 211 | } 212 | 213 | /** 214 | * Retrieves a previously registered hook. 215 | * 216 | * @param {string} className The name of the class to get the hook on. 217 | * @param {string} hookType One of 'beforeSave', 'afterSave', 'beforeDelete', 'afterDelete' 218 | */ 219 | function getHook(className, hookType) { 220 | if (hooks[className] && hooks[className][hookType]) { 221 | return hooks[className][hookType]; 222 | } 223 | return undefined; 224 | } 225 | 226 | function mockUser(_user) { 227 | user = _user; 228 | } 229 | 230 | function makeRequestObject(original, model, useMasterKey) { 231 | return { 232 | installationId: 'parse-mockdb', 233 | master: useMasterKey, 234 | object: model, 235 | original, 236 | user, 237 | }; 238 | } 239 | 240 | // Destructive. Takes data for update operation and removes all atomic operations. 241 | // Returns the extracted ops. 242 | function extractOps(data) { 243 | const ops = {}; 244 | 245 | _.forIn(data, (attribute, key) => { 246 | if (isOp(attribute)) { 247 | ops[key] = attribute; 248 | delete data[key]; 249 | } 250 | }); 251 | 252 | return ops; 253 | } 254 | 255 | // Destructive. Applies all the update `ops` to `data`. 256 | // Throws on unknown update operator. 257 | function applyOps(data, ops, className) { 258 | debugPrint('OPS', ops); 259 | _.forIn(ops, (value, key) => { 260 | const operator = value.__op; 261 | 262 | if (operator in UPDATE_OPERATORS) { 263 | UPDATE_OPERATORS[operator](data, key, value, className); 264 | } else { 265 | throw new Error(`Unknown update operator: ${key}`); 266 | } 267 | 268 | if (MASKED_UPDATE_OPS.has(operator)) { 269 | getMask(className).add(key); 270 | } 271 | }); 272 | } 273 | 274 | // Batch requests have the API version included in path 275 | function normalizePath(path) { 276 | return path.replace('/1/', ''); 277 | } 278 | 279 | const SPECIAL_CLASS_NAMES = { 280 | roles: '_Role', 281 | users: '_User', 282 | push: '_Push', 283 | }; 284 | 285 | /** 286 | * Given a class name and a where clause, returns DB matches by applying 287 | * the where clause (recursively if nested) 288 | */ 289 | function recursivelyMatch(className, where) { 290 | debugPrint('MATCH', { className, where }); 291 | const collection = getCollection(className); 292 | // eslint-disable-next-line no-use-before-define 293 | const matches = _.filter(_.values(collection), queryFilter(where)); 294 | debugPrint('MATCHES', { matches }); 295 | return _.cloneDeep(matches); // return copies instead of originals 296 | } 297 | 298 | // according to the js sdk api documentation parse uses the following radius of the earth 299 | const RADIUS_OF_EARTH_KM = 6371.0; 300 | const RADIUS_OF_EARTH_MILES = 3958.8; 301 | // the parse rest guide says that the maximum distance is 100 miles if no explicit maximum 302 | // is provided; here we already convert this distance into radians 303 | const DEFAULT_MAX_DISTANCE = 100 / RADIUS_OF_EARTH_MILES; 304 | 305 | /** 306 | * Operators for queries 307 | * 308 | * Params: 309 | * operand - the value on which the query operator is applied 310 | * value - operator value, i.e. the number 30 in `age: {$lt: 30}` 311 | */ 312 | const QUERY_OPERATORS = { 313 | $exists: (operand, value) => !!operand === value, 314 | $in: (operand, values) => _.some(values, value => objectsAreEqual(operand, value)), 315 | $nin: (operand, values) => _.every(values, value => !objectsAreEqual(operand, value)), 316 | $eq: (operand, value) => objectsAreEqual(operand, value), 317 | $ne: (operand, value) => !objectsAreEqual(operand, value), 318 | $lt: (operand, value) => operand < value, 319 | $lte: (operand, value) => operand <= value, 320 | $gt: (operand, value) => operand > value, 321 | $gte: (operand, value) => operand >= value, 322 | $regex: (operand, value) => { 323 | const regex = _.clone(value.$regex).replace(QUOTE_REGEXP, ''); 324 | return (new RegExp(regex, value.$options).test(operand)); 325 | }, 326 | $select: (operand, value) => { 327 | const foreignKey = value.key; 328 | const query = value.query; 329 | const matches = recursivelyMatch(query.className, query.where); 330 | const objectMatches = _.filter(matches, match => match[foreignKey] === operand); 331 | return objectMatches.length; 332 | }, 333 | $inQuery: (operand, query) => { 334 | const matches = recursivelyMatch(query.className, query.where); 335 | return _.find(matches, match => operand && match.objectId === operand.objectId); 336 | }, 337 | $all: (operand, value) => 338 | _.every(value, obj1 => _.some(operand, obj2 => objectsAreEqual(obj1, obj2))), 339 | $relatedTo: (operand, value) => { 340 | const object = value.object; 341 | const className = object.className; 342 | const id = object.objectId; 343 | const relatedKey = value.key; 344 | const relations = getCollection(className)[id][relatedKey] || []; 345 | // What is going on here? nothing is returned here? 346 | // TODO: could use a unit test to help document what's supposed to happen here 347 | if (indirect) { 348 | // Grab the className from the first relation item in order to set the class 349 | // correctly on the way out 350 | outOfBandResults = {}; 351 | if (relations && relations.length > 0) { 352 | outOfBandResults.className = relations[0].className; 353 | } 354 | outOfBandResults.matches = relations.reduce((results, relation) => { 355 | // eslint-disable-next-line no-use-before-define 356 | const matches = recursivelyMatch(relations[0].className, { 357 | objectId: relation.objectId, 358 | }); 359 | return results.concat(matches); 360 | }, []); 361 | } else { 362 | return objectsAreEqual(relations, operand); 363 | } 364 | return undefined; 365 | }, 366 | $nearSphere: (operand, value, additionalArgs) => { 367 | let maxDistance = additionalArgs.maxDistanceInRadians; 368 | 369 | if (_.isNil(maxDistance)) { 370 | maxDistance = DEFAULT_MAX_DISTANCE; 371 | } 372 | 373 | return new Parse.GeoPoint(operand).radiansTo(new Parse.GeoPoint(value)) <= maxDistance; 374 | }, 375 | // ignore these additional parameters for the $nearSphere op 376 | $maxDistance: () => true, 377 | $maxDistanceInRadians: () => true, 378 | $maxDistanceInKilometers: () => true, 379 | $maxDistanceInMiles: () => true, 380 | }; 381 | 382 | function evaluateObject(object, whereParams, key) { 383 | const nestedKeys = key.split('.'); 384 | if (nestedKeys.length > 1) { 385 | for (let i = 0; i < nestedKeys.length - 1; i++) { 386 | if (!object[nestedKeys[i]]) { 387 | // key not found 388 | return false; 389 | } 390 | object = object[nestedKeys[i]]; 391 | key = nestedKeys[i + 1]; 392 | } 393 | } 394 | 395 | if (typeof whereParams === 'object' && !Array.isArray(whereParams) && whereParams) { 396 | // Handle objects that actually represent scalar values 397 | if (isPointer(whereParams) || isDate(whereParams)) { 398 | return QUERY_OPERATORS.$eq.apply(null, [object[key], whereParams]); 399 | } 400 | 401 | if (key in QUERY_OPERATORS) { 402 | return QUERY_OPERATORS[key].apply(null, [object, whereParams]); 403 | } 404 | 405 | if ('$regex' in whereParams) { 406 | return QUERY_OPERATORS.$regex.apply(null, [object[key], whereParams]); 407 | } 408 | 409 | // $maxDistance... is not an operator for itself but just an additional parameter 410 | // for the $nearSphere operator, so we have to fetch this value in advance. 411 | const args = {}; 412 | if (whereParams) { 413 | args.maxDistanceInRadians = whereParams.$maxDistance || whereParams.$maxDistanceInRadians; 414 | if ('$maxDistanceInKilometers' in whereParams) { 415 | args.maxDistanceInRadians = whereParams.$maxDistanceInKilometers / RADIUS_OF_EARTH_KM; 416 | } else if ('$maxDistanceInMiles' in whereParams) { 417 | args.maxDistanceInRadians = whereParams.$maxDistanceInMiles / RADIUS_OF_EARTH_MILES; 418 | } 419 | } 420 | 421 | // Process each key in where clause to determine if we have a match 422 | return _.reduce(whereParams, (matches, value, constraint) => { 423 | const keyValue = deserializeQueryParam(object[key]); 424 | const param = deserializeQueryParam(value); 425 | 426 | // Constraint can take the form form of a query operator OR an equality match 427 | if (constraint in QUERY_OPERATORS) { // { age: {$lt: 30} } 428 | return matches && QUERY_OPERATORS[constraint].apply( 429 | null, 430 | [keyValue, param, args] 431 | ); 432 | } 433 | // { age: 30 } 434 | return matches && QUERY_OPERATORS.$eq.apply(null, [keyValue[constraint], param]); 435 | }, true); 436 | } 437 | 438 | return QUERY_OPERATORS.$eq.apply(null, [object[key], whereParams]); 439 | } 440 | 441 | 442 | /** 443 | * Returns a function that filters query matches on a where clause 444 | */ 445 | function queryFilter(where) { 446 | if (where.$or) { 447 | return object => 448 | _.reduce(where.$or, (result, subclause) => result || 449 | queryFilter(subclause)(object), false); 450 | } 451 | 452 | // Go through each key in where clause 453 | return object => _.reduce(where, (result, whereParams, key) => { 454 | const match = evaluateObject(object, whereParams, key); 455 | return result && match; 456 | }, true); 457 | } 458 | 459 | function handleRequest(method, path, body) { 460 | const explodedPath = normalizePath(path).split('/'); 461 | const start = explodedPath.shift(); 462 | const className = start === 'classes' ? explodedPath.shift() : SPECIAL_CLASS_NAMES[start]; 463 | 464 | const request = { 465 | method, 466 | className, 467 | data: body, 468 | objectId: explodedPath.shift(), 469 | }; 470 | 471 | try { 472 | // eslint-disable-next-line no-use-before-define 473 | return HANDLERS[method](request); 474 | } catch (e) { 475 | return Promise.reject(e); 476 | } 477 | } 478 | 479 | function respond(status, response) { 480 | return { 481 | status, 482 | response, 483 | }; 484 | } 485 | 486 | /** 487 | * Batch requests have the following form: { 488 | * requests: [ 489 | * { method, path, body }, 490 | * ] 491 | * } 492 | */ 493 | function handleBatchRequest(unused1, unused2, data) { 494 | const requests = data.requests; 495 | const getResults = requests.map(request => { 496 | const method = request.method; 497 | const path = request.path; 498 | const body = request.body; 499 | return handleRequest(method, path, body) 500 | .then(result => Promise.resolve({ success: result.response })); 501 | }); 502 | 503 | return Promise.all(getResults).then(results => respond(200, results)); 504 | } 505 | 506 | /** 507 | * Given an object, a pointer, or a JSON representation of a Parse Object, 508 | * return a fully fetched version of the Object. 509 | */ 510 | function fetchObjectByPointer(pointer) { 511 | const collection = getCollection(pointer.className); 512 | const storedItem = collection[pointer.objectId]; 513 | 514 | if (storedItem === undefined) { 515 | return undefined; 516 | } 517 | 518 | return Object.assign( 519 | { __type: 'Object', className: pointer.className }, 520 | _.cloneDeep(storedItem) 521 | ); 522 | } 523 | 524 | /** 525 | * Recursive function that traverses an include path and replaces pointers 526 | * with fully fetched objects 527 | */ 528 | function includePaths(object, pathsRemaining) { 529 | debugPrint('INCLUDE', { object, pathsRemaining }); 530 | const path = pathsRemaining.shift(); 531 | const target = object && object[path]; 532 | 533 | if (target) { 534 | if (Array.isArray(target)) { 535 | object[path] = target.map(item => { 536 | if (item.className) { 537 | // This is a pointer or an object 538 | const fetched = fetchObjectByPointer(item); 539 | includePaths(fetched, _.cloneDeep(pathsRemaining)); 540 | return fetched; 541 | } 542 | return item; 543 | }); 544 | } else { 545 | if (object[path].__type === 'Pointer') { 546 | object[path] = fetchObjectByPointer(target); 547 | } 548 | includePaths(object[path], pathsRemaining); 549 | } 550 | } 551 | 552 | return object; 553 | } 554 | 555 | /** 556 | * Given a set of matches of a GET query (e.g. find()), returns fully 557 | * fetched Parse Objects that include the nested objects requested by 558 | * Parse.Query.include() 559 | */ 560 | function queryMatchesAfterIncluding(matches, includeClause) { 561 | if (!includeClause) { 562 | return matches; 563 | } 564 | 565 | const includeClauses = includeClause.split(','); 566 | matches = _.map(matches, match => { 567 | for (let i = 0; i < includeClauses.length; i++) { 568 | const paths = includeClauses[i].split('.'); 569 | match = includePaths(match, paths); 570 | } 571 | return match; 572 | }); 573 | 574 | return matches; 575 | } 576 | 577 | /** 578 | * Sort query results if necessary 579 | */ 580 | function sortQueryresults(matches, order) { 581 | const orderArray = order.split(',').map(k => { 582 | let dir = 'asc'; 583 | let key = k; 584 | 585 | if (k.charAt(0) === '-') { 586 | key = k.substring(1); 587 | dir = 'desc'; 588 | } 589 | 590 | return [item => deserializeQueryParam(item[key]), dir]; 591 | }); 592 | 593 | const keys = orderArray.map(_.first); 594 | const orders = orderArray.map(_.last); 595 | 596 | return _.orderBy(matches, keys, orders); 597 | } 598 | 599 | /** 600 | * Handles a GET request (Parse.Query.find(), get(), first(), Parse.Object.fetch()) 601 | */ 602 | function handleGetRequest(request) { 603 | const objId = request.objectId; 604 | const className = request.className; 605 | if (objId) { 606 | // Object.fetch() query 607 | const collection = getCollection(className); 608 | const currentObject = collection[objId]; 609 | if (!currentObject) { 610 | return Promise.resolve(respond(404, { 611 | code: 101, 612 | error: 'object not found for update', 613 | })); 614 | } 615 | let match = _.cloneDeep(currentObject); 616 | 617 | if (match) { 618 | const toOmit = Array.from(getMask(className)); 619 | match = _.omit(match, toOmit); 620 | } 621 | 622 | return Promise.resolve(respond(200, match)); 623 | } 624 | const data = request.data; 625 | indirect = data.redirectClassNameForKey; 626 | let matches = recursivelyMatch(className, data.where); 627 | let matchesClassName = ''; 628 | if (indirect) { 629 | matches = outOfBandResults.matches; 630 | if (outOfBandResults.className) { 631 | matchesClassName = outOfBandResults.className; 632 | } 633 | } 634 | 635 | if (request.data.count) { 636 | return Promise.resolve(respond(200, { count: matches.length })); 637 | } 638 | 639 | matches = queryMatchesAfterIncluding(matches, data.include); 640 | 641 | const toOmit = Array.from(getMask(className)); 642 | matches = matches.map((match) => _.omit(match, toOmit)); 643 | 644 | // TODO: Can we just call toJSON() in order to avoid this? 645 | matches.forEach(match => { 646 | if (match.createdAt) { 647 | match.createdAt = match.createdAt.toJSON(); 648 | } 649 | if (match.updatedAt) { 650 | match.updatedAt = match.updatedAt.toJSON(); 651 | } 652 | }); 653 | 654 | // sort results if necessary 655 | if (data.order && data.order.length > 0 && matches.length > 0) { 656 | matches = sortQueryresults(matches, data.order); 657 | } 658 | 659 | const limit = data.limit || DEFAULT_LIMIT; 660 | const startIndex = data.skip || 0; 661 | const endIndex = startIndex + limit; 662 | const response = { results: matches.slice(startIndex, endIndex) }; 663 | 664 | // Add the class name for the outgoing objects to the response if sepcified 665 | if (matchesClassName.length > 0) { 666 | response.className = matchesClassName; 667 | } 668 | 669 | return Promise.resolve(respond(200, response)); 670 | } 671 | 672 | /** 673 | * Executes a registered hook with data provided. 674 | * 675 | * Hydrates the data into an instance of the class named by `className` param and binds it to the 676 | * function to be run. 677 | * 678 | * @param {string} className The name of the class to get the hook on. 679 | * @param {string} hookType One of 'beforeSave', 'afterSave', 'beforeDelete', 'afterDelete' 680 | * @param {Object} data The Data that is to be hydrated into an instance of className class. 681 | */ 682 | function runHook(className, hookType, data) { 683 | let hook = getHook(className, hookType); 684 | if (hook) { 685 | const hydrate = (rawData) => { 686 | const modelData = Object.assign({}, rawData, { className }); 687 | const modelJSON = _.mapValues(modelData, 688 | // Convert dates into JSON loadable representations 689 | value => ((value instanceof Date) ? value.toJSON() : value) 690 | ); 691 | return Parse.Object.fromJSON(modelJSON); 692 | }; 693 | const model = hydrate(data, className); 694 | hook = hook.bind(model); 695 | 696 | const collection = getCollection(className); 697 | let original; 698 | if (collection[model.id]) { 699 | original = hydrate(collection[model.id]); 700 | } 701 | // TODO Stub out Parse.Cloud.useMasterKey() so that we can report the correct 'master' 702 | // value here. 703 | return hook(makeRequestObject(original, model, false)).then((beforeSaveOverrideValue) => { 704 | debugPrint('HOOK', { beforeSaveOverrideValue }); 705 | 706 | // Unlike BeforeDeleteResponse, BeforeSaveResponse might specify 707 | let objectToProceedWith = model; 708 | if (hookType === 'beforeSave' && beforeSaveOverrideValue) { 709 | objectToProceedWith = beforeSaveOverrideValue.toJSON(); 710 | } 711 | 712 | return Promise.resolve(objectToProceedWith); 713 | }); 714 | } 715 | return Promise.resolve(data); 716 | } 717 | 718 | function getChangedKeys(originalObject, updatedObject) { 719 | if (originalObject === updatedObject) { 720 | return []; 721 | } 722 | return _.reduce(updatedObject, (result, value, key) => { 723 | if (!_.isEqual(originalObject[key], value)) { 724 | result.push(key); 725 | } 726 | return result; 727 | }, []); 728 | } 729 | 730 | /** 731 | * Handles a POST request (Parse.Object.save()) 732 | */ 733 | function handlePostRequest(request) { 734 | const className = request.className; 735 | const collection = getCollection(className); 736 | 737 | let newObject; 738 | return runHook(className, 'beforeSave', request.data).then(result => { 739 | const changedKeys = getChangedKeys(request.data, result); 740 | 741 | const newId = crypto.newObjectId(); 742 | const now = new Date(); 743 | 744 | const ops = extractOps(result); 745 | 746 | newObject = Object.assign( 747 | result, 748 | { objectId: newId, createdAt: now, updatedAt: now } 749 | ); 750 | 751 | applyOps(newObject, ops, className); 752 | const toOmit = ['updatedAt'].concat(Array.from(getMask(className))); 753 | const toPick = Object.keys(ops).concat(changedKeys); 754 | 755 | collection[newId] = newObject; 756 | 757 | const response = Object.assign( 758 | _.cloneDeep(_.omit(_.pick(result, toPick), toOmit)), 759 | { objectId: newId, createdAt: result.createdAt.toJSON() } 760 | ); 761 | 762 | return Promise.resolve(respond(201, response)); 763 | }).then((result) => { 764 | runHook(className, 'afterSave', newObject); 765 | return result; 766 | }); 767 | } 768 | 769 | function handlePutRequest(request) { 770 | const className = request.className; 771 | const collection = getCollection(className); 772 | const objId = request.objectId; 773 | const currentObject = collection[objId]; 774 | const now = new Date(); 775 | const data = request.data || {}; 776 | 777 | const ops = extractOps(data); 778 | 779 | if (!currentObject) { 780 | return Promise.resolve(respond(404, { 781 | code: 101, 782 | error: 'object not found for put', 783 | })); 784 | } 785 | 786 | const updatedObject = Object.assign( 787 | _.cloneDeep(currentObject), 788 | data, 789 | { updatedAt: now } 790 | ); 791 | 792 | applyOps(updatedObject, ops, className); 793 | const toOmit = ['createdAt', 'objectId'].concat(Array.from(getMask(className))); 794 | 795 | return runHook(className, 'beforeSave', updatedObject).then(result => { 796 | const changedKeys = getChangedKeys(updatedObject, result); 797 | 798 | collection[request.objectId] = updatedObject; 799 | const response = Object.assign( 800 | _.cloneDeep(_.omit(_.pick(result, Object.keys(ops).concat(changedKeys)), toOmit)), 801 | { updatedAt: now.toJSON() } 802 | ); 803 | return Promise.resolve(respond(200, response)); 804 | }).then((result) => { 805 | runHook(className, 'afterSave', updatedObject); 806 | return result; 807 | }); 808 | } 809 | 810 | function handleDeleteRequest(request) { 811 | const collection = getCollection(request.className); 812 | const objToDelete = collection[request.objectId]; 813 | 814 | return runHook(request.className, 'beforeDelete', objToDelete).then(() => { 815 | delete collection[request.objectId]; 816 | return Promise.resolve(respond(200, {})); 817 | }); 818 | } 819 | 820 | const HANDLERS = { 821 | GET: handleGetRequest, 822 | POST: handlePostRequest, 823 | PUT: handlePutRequest, 824 | DELETE: handleDeleteRequest, 825 | }; 826 | 827 | const MockRESTController = { 828 | request: (method, path, data, options) => { 829 | let result; 830 | if (path === 'batch') { 831 | debugPrint('BATCH', { method, path, data, options }); 832 | result = handleBatchRequest(method, path, data); 833 | } else { 834 | debugPrint('REQUEST', { method, path, data, options }); 835 | result = handleRequest(method, path, data); 836 | } 837 | 838 | return result.then(finalResult => { 839 | // Status of database after handling request above 840 | debugPrint('DB', db); 841 | debugPrint('RESPONSE', finalResult.response); 842 | return Promise.resolve(finalResult.response); 843 | }); 844 | }, 845 | ajax: () => { 846 | /* no-op */ 847 | }, 848 | }; 849 | 850 | /** 851 | * Mocks a Parse API server, by intercepting requests and storing/querying data locally 852 | * in an in-memory DB. 853 | */ 854 | function mockDB(parseModule) { 855 | Parse = parseModule; 856 | if (!mocked) { 857 | defaultController = Parse.CoreManager.getRESTController(); 858 | mocked = true; 859 | Parse.CoreManager.setRESTController(MockRESTController); 860 | } 861 | } 862 | 863 | /** 864 | * Restores the original RESTController. 865 | */ 866 | function unMockDB() { 867 | if (mocked) { 868 | Parse.CoreManager.setRESTController(defaultController); 869 | mocked = false; 870 | } 871 | } 872 | 873 | const MockDB = { 874 | mockDB, 875 | unMockDB, 876 | cleanUp, 877 | registerHook, 878 | mockUser, 879 | }; 880 | 881 | module.exports = MockDB; 882 | -------------------------------------------------------------------------------- /test/test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const assert = require('assert'); 4 | const ParseMockDB = require('../src/parse-mockdb'); 5 | const Parse = require('parse/node'); 6 | 7 | class Brand extends Parse.Object { 8 | constructor(attributes, options) { 9 | super('Brand', attributes, options); 10 | } 11 | } 12 | Parse.Object.registerSubclass('Brand', Brand); 13 | 14 | class Item extends Parse.Object { 15 | constructor(attributes, options) { 16 | super('Item', attributes, options); 17 | } 18 | } 19 | Parse.Object.registerSubclass('Item', Item); 20 | 21 | class Store extends Parse.Object { 22 | constructor(attributes, options) { 23 | super('Store', attributes, options); 24 | } 25 | } 26 | Parse.Object.registerSubclass('Store', Store); 27 | 28 | class CustomUserSubclass extends Parse.User { } 29 | 30 | function createBrandP(name) { 31 | const brand = new Brand(); 32 | brand.set('name', name); 33 | return brand.save(); 34 | } 35 | 36 | function createItemP(price, brand, extra) { 37 | const item = new Item(); 38 | item.set('price', price); 39 | 40 | if (brand) { 41 | item.set('brand', brand); 42 | } 43 | 44 | if (extra) { 45 | item.set(extra); 46 | } 47 | 48 | return item.save(); 49 | } 50 | 51 | function createStoreWithItemP(item) { 52 | const store = new Store(); 53 | store.set('item', item); 54 | return store.save(); 55 | } 56 | 57 | function createUserP(name) { 58 | const user = new CustomUserSubclass(); 59 | user.set('name', name); 60 | return user.save(); 61 | } 62 | 63 | function itemQueryP(price) { 64 | const query = new Parse.Query(Item); 65 | query.equalTo('price', price); 66 | return query.find(); 67 | } 68 | 69 | function behavesLikeParseObjectOnBeforeSave(typeName, ParseObjectOrUserSubclass) { 70 | context('when object has beforeSave hook registered', () => { 71 | function beforeSavePromise(request) { 72 | const { original, object } = request; 73 | if (object.get('error')) { 74 | return Promise.reject('whoah'); 75 | } 76 | if (original && object.get('value') <= original.get('value')) { 77 | return Promise.reject('The value can only go up, not down'); 78 | } 79 | object.set('cool', true); 80 | return Promise.resolve(object); 81 | } 82 | 83 | it('runs the hook before saving the model and persists the object', () => { 84 | ParseMockDB.registerHook(typeName, 'beforeSave', beforeSavePromise); 85 | 86 | const object = new ParseObjectOrUserSubclass(); 87 | assert(!object.has('cool')); 88 | 89 | return object.save().then(savedObject => { 90 | assert(savedObject.has('cool')); 91 | assert(savedObject.get('cool')); 92 | 93 | return new Parse.Query(ParseObjectOrUserSubclass).first().then(queriedObject => { 94 | assert(queriedObject.has('cool')); 95 | assert(queriedObject.get('cool')); 96 | }); 97 | }); 98 | }); 99 | 100 | it('rejects the save if there is a problem', () => { 101 | ParseMockDB.registerHook(typeName, 'beforeSave', beforeSavePromise); 102 | 103 | const object = new ParseObjectOrUserSubclass({ error: true }); 104 | 105 | return object.save().then(() => { 106 | assert.fail(null, null, 'should not have saved'); 107 | }, error => { 108 | assert.equal(error, 'whoah'); 109 | }); 110 | }); 111 | 112 | it('rejects the save if there is a problem based on the previous value', () => { 113 | ParseMockDB.registerHook(typeName, 'beforeSave', beforeSavePromise); 114 | 115 | const object = new ParseObjectOrUserSubclass({ value: 4 }); 116 | return object.save().then(() => { 117 | object.set('value', 3); 118 | return object.save(); 119 | }).then(() => { 120 | assert.fail(null, null, 'should not have saved'); 121 | }, error => { 122 | assert.equal(error, 'The value can only go up, not down'); 123 | }); 124 | }); 125 | }); 126 | } 127 | 128 | function behavesLikeParseObjectOnBeforeDelete(typeName, ParseObjectOrUserSubclass) { 129 | context('when object has beforeDelete hook registered', () => { 130 | let beforeDeleteWasRun; 131 | 132 | beforeEach(() => { 133 | beforeDeleteWasRun = false; 134 | }); 135 | 136 | function beforeDeletePromise(request) { 137 | const object = request.object; 138 | if (object.get('error')) { 139 | return Promise.reject('whoah'); 140 | } 141 | beforeDeleteWasRun = true; 142 | return Promise.resolve(); 143 | } 144 | 145 | it('runs the hook before deleting the object', () => { 146 | ParseMockDB.registerHook(typeName, 'beforeDelete', beforeDeletePromise); 147 | 148 | const promises = []; 149 | 150 | promises.push(new ParseObjectOrUserSubclass() 151 | .save() 152 | .then(savedParseObjectOrUserSubclass => 153 | Parse.Object.destroyAll([savedParseObjectOrUserSubclass])) 154 | .then(() => assert(beforeDeleteWasRun)) 155 | ); 156 | 157 | promises.push(new Parse.Query(ParseObjectOrUserSubclass) 158 | .find() 159 | .then(results => { 160 | assert.equal(results.length, 0); 161 | })); 162 | 163 | return Promise.all(promises); 164 | }); 165 | 166 | it('rejects the delete if there is a problem', () => { 167 | ParseMockDB.registerHook(typeName, 'beforeDelete', beforeDeletePromise); 168 | 169 | const object = new ParseObjectOrUserSubclass({ error: true }); 170 | return object.save().then(savedParseObjectOrUserSubclass => 171 | Parse.Object.destroyAll([savedParseObjectOrUserSubclass]) 172 | ).then(() => { 173 | assert.fail(null, null, 'should not have deleted'); 174 | }, (error) => { 175 | assert.equal(error, 'whoah'); 176 | return new Parse.Query(ParseObjectOrUserSubclass).find(); 177 | }).then((results) => { 178 | assert.equal(results.length, 1); 179 | }); 180 | }); 181 | }); 182 | } 183 | 184 | function behavesLikeParseObjectOnAfterSave(typeName, ParseObjectOrUserSubclass) { 185 | context('when object has afterSave hook registered', () => { 186 | let didAfterSave; 187 | let objectInAfterSave; 188 | function afterSavePromise(request) { 189 | didAfterSave = true; 190 | objectInAfterSave = request.object; 191 | return Promise.resolve(); 192 | } 193 | 194 | beforeEach(() => { 195 | didAfterSave = false; 196 | objectInAfterSave = {}; 197 | }); 198 | 199 | context('when saving a new object', () => { 200 | it('runs the hook after saving the model and persisting the object', () => { 201 | ParseMockDB.registerHook(typeName, 'afterSave', afterSavePromise); 202 | const object = new ParseObjectOrUserSubclass(); 203 | return object.save().then(() => assert(didAfterSave)); 204 | }); 205 | 206 | it("get all the object's attributes during the afterSave hook", () => { 207 | ParseMockDB.registerHook(typeName, 'afterSave', afterSavePromise); 208 | const object = new ParseObjectOrUserSubclass({ name: 'abc' }); 209 | return object.save().then((savedObject) => { 210 | assert(didAfterSave); 211 | 212 | assert.equal( 213 | objectInAfterSave.get('createdAt').getTime(), 214 | savedObject.get('createdAt').getTime() 215 | ); 216 | 217 | assert.equal( 218 | objectInAfterSave.get('updatedAt').getTime(), 219 | savedObject.get('updatedAt').getTime() 220 | ); 221 | 222 | assert.equal( 223 | objectInAfterSave.id, 224 | savedObject.id 225 | ); 226 | 227 | assert.equal( 228 | objectInAfterSave.get('name'), 229 | savedObject.get('name') 230 | ); 231 | }); 232 | }); 233 | 234 | context('when the afterSave hook hits an error', () => { 235 | beforeEach(() => { 236 | const badHook = () => Promise.reject(new Error('Something went wrong')); 237 | ParseMockDB.registerHook(typeName, 'afterSave', badHook); 238 | }); 239 | 240 | it('still saves the object', () => { 241 | const object = new ParseObjectOrUserSubclass(); 242 | return object.save().then((savedObject) => { 243 | assert(!!savedObject.id); 244 | }); 245 | }); 246 | }); 247 | }); 248 | 249 | context('when updating an existing object', () => { 250 | let object; 251 | beforeEach(() => { 252 | // Tricky: We're creating this object before registering the hook, 253 | // so it won't fire here. 254 | object = new ParseObjectOrUserSubclass({ name: 'original' }); 255 | return object.save(); 256 | }); 257 | 258 | it('runs the hook after saving the model and persisting the object', () => { 259 | ParseMockDB.registerHook(typeName, 'afterSave', afterSavePromise); 260 | object.set('name', 'updated'); 261 | return object.save().then(() => assert(didAfterSave)); 262 | }); 263 | 264 | it("get all the object's attributes during the afterSave hook", () => { 265 | ParseMockDB.registerHook(typeName, 'afterSave', afterSavePromise); 266 | object.set('name', 'updated'); 267 | return object.save().then((savedObject) => { 268 | assert(didAfterSave); 269 | 270 | assert.equal( 271 | objectInAfterSave.get('createdAt').getTime(), 272 | savedObject.get('createdAt').getTime() 273 | ); 274 | 275 | assert.equal( 276 | objectInAfterSave.get('updatedAt').getTime(), 277 | savedObject.get('updatedAt').getTime() 278 | ); 279 | 280 | assert.equal( 281 | objectInAfterSave.id, 282 | savedObject.id 283 | ); 284 | 285 | assert.equal( 286 | objectInAfterSave.get('name'), 287 | 'updated' 288 | ); 289 | }); 290 | }); 291 | 292 | context('when the afterSave hook hits an error', () => { 293 | beforeEach(() => { 294 | const badHook = () => Promise.reject(new Error('Something went wrong')); 295 | ParseMockDB.registerHook(typeName, 'afterSave', badHook); 296 | }); 297 | 298 | it('still saves the object', () => { 299 | object.set('name', 'updated'); 300 | return object.save().then((savedObject) => { 301 | assert(!!savedObject.id); 302 | }); 303 | }); 304 | }); 305 | }); 306 | }); 307 | } 308 | 309 | function sleep(ms) { 310 | return new Promise(resolve => setTimeout(resolve, ms)); 311 | } 312 | 313 | describe('ParseMock', () => { 314 | beforeEach(() => { 315 | ParseMockDB.mockDB(Parse); 316 | }); 317 | 318 | afterEach(() => { 319 | ParseMockDB.cleanUp(); 320 | }); 321 | 322 | context('supports Parse.User subclasses', () => { 323 | it('should save user', () => 324 | createUserP('Tom').then((user) => { 325 | assert.equal(user.get('name'), 'Tom'); 326 | }) 327 | ); 328 | 329 | it('should save and find a user', () => 330 | createUserP('Tom').then(() => { 331 | const query = new Parse.Query(CustomUserSubclass); 332 | query.equalTo('name', 'Tom'); 333 | return query.first().then((user) => { 334 | assert.equal(user.get('name'), 'Tom'); 335 | }); 336 | }) 337 | ); 338 | 339 | behavesLikeParseObjectOnBeforeSave('_User', CustomUserSubclass); 340 | behavesLikeParseObjectOnBeforeDelete('_User', CustomUserSubclass); 341 | behavesLikeParseObjectOnAfterSave('_User', CustomUserSubclass); 342 | }); 343 | 344 | it('should save correctly', () => 345 | createItemP(30).then((item) => { 346 | assert.equal(item.get('price'), 30); 347 | }) 348 | ); 349 | 350 | it('should come back with createdAt', () => { 351 | let createdAt; 352 | return createItemP(30).then((item) => { 353 | assert(item.createdAt); 354 | createdAt = item.createdAt; 355 | return (new Parse.Query(Item)).first(); 356 | }).then((fetched) => { 357 | assert.equal(createdAt.getTime(), fetched.createdAt.getTime()); 358 | }); 359 | }); 360 | 361 | it('should get a specific ID correctly', () => 362 | createItemP(30).then(item => { 363 | const query = new Parse.Query(Item); 364 | return query.get(item.id).then(fetchedItem => { 365 | assert.equal(fetchedItem.id, item.id); 366 | }); 367 | }) 368 | ); 369 | 370 | it('should match a correct equalTo query on price', () => 371 | createItemP(30) 372 | .then((item) => itemQueryP(30) 373 | .then(results => { 374 | assert.equal(results[0].id, item.id); 375 | assert.equal(results[0].get('price'), item.get('price')); 376 | }) 377 | ) 378 | ); 379 | 380 | it('should match a query that uses equalTo as contains constraint', () => 381 | createItemP(30) 382 | .then((item) => 383 | new Parse.Object('Factory').save({ 384 | items: [item], 385 | }) 386 | .then(savedComp => new Parse.Query('Factory') 387 | .equalTo('items', item) 388 | .find() 389 | .then(results => { 390 | assert.equal(results[0].id, savedComp.id); 391 | }) 392 | ) 393 | ) 394 | ); 395 | 396 | it('should match a query that uses equalTo as contains constraint with 0 as parameter', () => 397 | new Parse.Object('Factory').save({ 398 | items: [0, 1], 399 | }).then(savedComp => new Parse.Query('Factory') 400 | .equalTo('items', 0) 401 | .find() 402 | .then(results => { 403 | assert.equal(results[0].id, savedComp.id); 404 | }) 405 | ) 406 | ); 407 | 408 | it('should not allow array values as equalTo parameter for array columns', () => 409 | new Parse.Object('Factory').save({ 410 | items: [0, 1], 411 | }).then(() => new Parse.Query('Factory') 412 | .equalTo('items', [0, 1]) 413 | .find() 414 | .then(() => Promise.reject( 415 | new Error('Promise should have failed')), 416 | () => Promise.resolve(true)) 417 | ) 418 | ); 419 | 420 | it('should not match objects with [] as field value and 0 as query parameter', () => 421 | new Parse.Object('Factory').save({ 422 | items: [], 423 | }).then(() => new Parse.Query('Factory') 424 | .equalTo('items', 0) 425 | .find() 426 | .then(results => { 427 | assert.equal(results.length, 0); 428 | }) 429 | ) 430 | ); 431 | 432 | it('should not match objects with null as field value and \'\' as query parameter', () => 433 | new Parse.Object('Factory').save({ 434 | name: null, 435 | }).then(() => new Parse.Query('Factory') 436 | .equalTo('items', '') 437 | .find() 438 | .then(results => { 439 | assert.equal(results.length, 0); 440 | }) 441 | ) 442 | ); 443 | 444 | it('should save and find an item', () => { 445 | const item = new Item(); 446 | item.set('price', 30); 447 | return item.save() 448 | .then(() => { 449 | const query = new Parse.Query(Item); 450 | query.equalTo('price', 30); 451 | return query.first().then(returnedItem => { 452 | assert.equal(returnedItem.get('price'), 30); 453 | }); 454 | }); 455 | }); 456 | 457 | it('should save and find an item via object comparison', () => { 458 | const startItem = new Item({ cool: { awesome: true } }); 459 | return startItem.save().then(() => { 460 | const query = new Parse.Query(Item); 461 | query.equalTo('cool', { awesome: true }); 462 | return query.first().then((item) => { 463 | assert(item.get('cool').awesome); 464 | }); 465 | }); 466 | }); 467 | 468 | it('should save a nested item and return it with the save', () => 469 | new Item().save({ 470 | price: 45, 471 | }).then((item0) => 472 | new Item().save({ 473 | price: 50, 474 | }).then((item2) => 475 | new Item({ 476 | price: 55, 477 | }).save().then((item3) => { 478 | const brand = new Brand(); 479 | const item1 = new Item(); 480 | item1.id = item0.id; // create pointer to item0 481 | brand.set('items', [item1, item2, item3]); 482 | return brand.save(); 483 | }) 484 | ) 485 | ).then((brand) => { 486 | assert.equal(brand.get('items')[0].get('price'), undefined); 487 | assert.equal(brand.get('items')[1].get('price'), 50); 488 | assert.equal(brand.get('items')[2].get('price'), 55); 489 | 490 | brand.get('items')[2].set('price', 30); 491 | brand.set('name', 'foo'); 492 | return brand.save(); 493 | }) 494 | .then((sbrand) => { 495 | assert.equal(sbrand.get('items')[0].get('price'), undefined); 496 | assert.equal(sbrand.get('items')[1].get('price'), 50); 497 | assert.equal(sbrand.get('items')[2].get('price'), 30); 498 | }) 499 | ); 500 | 501 | it('should save a nested item and return it with the save even with a hook defined', () => { 502 | ParseMockDB.registerHook('Brand', 'beforeSave', request => { 503 | const object = request.object; 504 | object.set('name', 'bar'); 505 | return Promise.resolve(object); 506 | }); 507 | 508 | return new Item().save({ 509 | price: 45, 510 | }).then((item0) => 511 | new Item().save({ 512 | price: 50, 513 | }).then((item2) => 514 | new Item({ 515 | price: 55, 516 | }).save().then((item3) => { 517 | const brand = new Brand(); 518 | const item1 = new Item(); 519 | item1.id = item0.id; // create pointer to item0 520 | brand.set('items', [item1, item2, item3]); 521 | return brand.save(); 522 | }) 523 | ) 524 | ).then((brand) => { 525 | assert.equal(brand.get('items')[0].get('price'), undefined); 526 | assert.equal(brand.get('items')[1].get('price'), 50); 527 | assert.equal(brand.get('items')[2].get('price'), 55); 528 | 529 | brand.get('items')[2].set('price', 30); 530 | brand.set('name', 'foo'); 531 | return brand.save(); 532 | }) 533 | .then((sbrand) => { 534 | assert.equal(sbrand.get('items')[0].get('price'), undefined); 535 | assert.equal(sbrand.get('items')[1].get('price'), 50); 536 | assert.equal(sbrand.get('items')[2].get('price'), 30); 537 | }); 538 | }); 539 | 540 | it('should support increment', () => 541 | createItemP(30).then((item) => { 542 | item.increment('price', 5); 543 | return item.save(); 544 | }).then((item) => { 545 | assert.equal(item.get('price'), 35); 546 | }) 547 | ); 548 | 549 | it('should support negative increment', () => 550 | createItemP(30).then((item) => { 551 | item.increment('price', -5); 552 | return item.save(); 553 | }).then((item) => { 554 | assert.equal(item.get('price'), 25); 555 | }) 556 | ); 557 | 558 | it('should increment a non-existent field', () => 559 | createItemP(30).then((item) => 560 | item 561 | .increment('foo') 562 | .save() 563 | ).then((item) => { 564 | assert.equal(item.get('foo'), 1); 565 | }) 566 | ); 567 | 568 | it('should match an item that is within a kilometer radius of a geo point', () => 569 | // the used two points are 133.4 km away according to http://www.movable-type.co.uk/scripts/latlong.html 570 | new Item().save({ 571 | location: new Parse.GeoPoint(49, 7), 572 | }).then(item => 573 | new Parse.Query(Item) 574 | .withinKilometers('location', new Parse.GeoPoint(48, 8), 134) 575 | .find() 576 | .then(results => { 577 | assert.equal(results[0].id, item.id); 578 | }) 579 | ) 580 | ); 581 | 582 | it('should not match an item that is not within a kilometer radius of a geo point', () => 583 | // the used two points are 133.4 km away according to http://www.movable-type.co.uk/scripts/latlong.html 584 | new Item().save({ 585 | location: new Parse.GeoPoint(49, 7), 586 | }).then(() => 587 | new Parse.Query(Item) 588 | .withinKilometers('location', new Parse.GeoPoint(48, 8), 133) 589 | .find() 590 | ).then(results => { 591 | assert.equal(results.length, 0); 592 | }) 593 | ); 594 | 595 | xit('should sort matches of a geo query from nearest to furthest', () => 596 | // the used two points are 133.4 km away according to http://www.movable-type.co.uk/scripts/latlong.html 597 | new Item().save({ 598 | location: new Parse.GeoPoint(49, 7), 599 | }).then(item1 => 600 | new Item().save({ 601 | location: new Parse.GeoPoint(49, 8), 602 | }).then(item2 => 603 | new Parse.Query(Item) 604 | .withinKilometers('location', new Parse.GeoPoint(48, 8), 134) 605 | .find() 606 | .then(results => { 607 | assert.equal(results[0].id, item2.id); 608 | assert.equal(results[1].id, item1.id); 609 | }) 610 | ) 611 | ) 612 | ); 613 | 614 | it('should use a custom order over ordering from nearest to furthest in a geo query', () => 615 | // the used two points are 133.4 km away according to http://www.movable-type.co.uk/scripts/latlong.html 616 | new Item().save({ 617 | price: 21, 618 | location: new Parse.GeoPoint(49, 7), 619 | }).then(item1 => 620 | new Item().save({ 621 | price: 20, 622 | location: new Parse.GeoPoint(49, 8), 623 | }).then(item2 => 624 | new Parse.Query(Item) 625 | .withinKilometers('location', new Parse.GeoPoint(48, 8), 134) 626 | .ascending('price') 627 | .find() 628 | .then(results => { 629 | assert.equal(results[0].id, item2.id); 630 | assert.equal(results[1].id, item1.id); 631 | }) 632 | ) 633 | ) 634 | ); 635 | 636 | it('should use a descending order for query', () => 637 | new Item().save({ 638 | price: 21, 639 | }).then(item1 => 640 | new Item().save({ 641 | price: 25, 642 | }).then(item2 => 643 | new Item().save({ 644 | price: 20, 645 | }).then(item3 => 646 | new Parse.Query(Item) 647 | .descending('price') 648 | .find() 649 | .then(results => { 650 | assert.equal(results[0].id, item2.id); 651 | assert.equal(results[1].id, item1.id); 652 | assert.equal(results[2].id, item3.id); 653 | }) 654 | ) 655 | ) 656 | ) 657 | ); 658 | 659 | it('should use a descending order for text queries', () => 660 | new Item().save({ 661 | moniker: 'Meerkat', 662 | }).then(item1 => 663 | new Item().save({ 664 | moniker: 'Aardvaark', 665 | }).then(item2 => 666 | new Item().save({ 667 | moniker: 'Zebra', 668 | }).then(item3 => 669 | new Parse.Query(Item) 670 | .descending('moniker') 671 | .find() 672 | .then(results => { 673 | assert.equal(results[0].id, item3.id); 674 | assert.equal(results[1].id, item1.id); 675 | assert.equal(results[2].id, item2.id); 676 | }) 677 | ) 678 | ) 679 | ) 680 | ); 681 | 682 | it('should use an ascending order for date query', () => 683 | new Item().save({ 684 | expires: new Date(2017, 0, 2), 685 | }).then(item1 => 686 | new Item().save({ 687 | expires: new Date(2017, 0, 1), 688 | }).then(item2 => 689 | new Parse.Query(Item) 690 | .ascending('expires') 691 | .find() 692 | .then(results => { 693 | assert.equal(results[0].id, item2.id); 694 | assert.equal(results[1].id, item1.id); 695 | }) 696 | ) 697 | ) 698 | ); 699 | 700 | it('should use an descending order a query on createdAt', () => 701 | new Item().save().then(item1 => 702 | // we need to make sure the created at dates are different! 703 | sleep(1).then(() => 704 | new Item().save() 705 | ).then(item2 => 706 | new Parse.Query(Item) 707 | .descending('createdAt') 708 | .find() 709 | .then(results => { 710 | assert.equal(results[0].id, item2.id); 711 | assert.equal(results[1].id, item1.id); 712 | }) 713 | ) 714 | ) 715 | ); 716 | 717 | it('should support multiple sorting parameters', () => 718 | new Item().save({ 719 | active: true, 720 | price: 20, 721 | }).then(item1 => 722 | new Item().save({ 723 | active: true, 724 | price: 21, 725 | }).then(item2 => 726 | new Item().save({ 727 | active: false, 728 | price: 20, 729 | }).then(item3 => 730 | new Parse.Query(Item) 731 | .descending('active') 732 | .addAscending('price') 733 | .find() 734 | .then(results => { 735 | assert.equal(results[0].id, item1.id); 736 | assert.equal(results[1].id, item2.id); 737 | assert.equal(results[2].id, item3.id); 738 | }) 739 | ) 740 | ) 741 | ) 742 | ); 743 | 744 | it('should support unset', () => 745 | createItemP(30).then((item) => { 746 | item.unset('price'); 747 | return item.save(); 748 | }).then((item) => { 749 | assert(!item.has('price')); 750 | }) 751 | ); 752 | 753 | it('should support add', () => 754 | createItemP(30).then((item) => { 755 | item.add('languages', 'JS'); 756 | return item.save(); 757 | }).then((item) => { 758 | assert.deepEqual(item.get('languages'), ['JS']); 759 | }) 760 | ); 761 | 762 | it('should support addUnique', () => 763 | createItemP(30).then((item) => { 764 | item.add('languages', 'JS'); 765 | item.add('languages', 'Ruby'); 766 | return item.save(); 767 | }).then((item) => { 768 | assert.deepEqual(item.get('languages'), ['JS', 'Ruby']); 769 | item.addUnique('languages', 'JS'); 770 | return item.save(); 771 | }).then((item) => { 772 | assert.deepEqual(item.get('languages'), ['JS', 'Ruby']); 773 | }) 774 | ); 775 | 776 | it('should support addUnique with parse objects', () => 777 | new Item().save().then(i => 778 | new Brand() 779 | .save() 780 | .then(b => { 781 | b.addUnique('items', i); 782 | return b.save(); 783 | }) 784 | .then(b => { 785 | b.addUnique('items', i); 786 | return b.save(); 787 | }) 788 | .then(b => b.fetch()) 789 | .then(b => { 790 | assert.equal(b.get('items').length, 1); 791 | assert.equal(b.get('items')[0].id, i.id); 792 | }) 793 | ) 794 | ); 795 | 796 | it('should support addUnique with dates', () => 797 | new Item() 798 | .save() 799 | .then(i => { 800 | i.addUnique('dates', new Date(5)); 801 | return i.save(); 802 | }) 803 | .then(i => { 804 | i.addUnique('dates', new Date(5)); 805 | return i.save(); 806 | }) 807 | .then(i => i.fetch()) 808 | .then(i => { 809 | assert.equal(i.get('dates').length, 1); 810 | assert.equal(i.get('dates')[0].getTime(), 5); 811 | }) 812 | ); 813 | 814 | it('should support remove', () => 815 | createItemP(30).then((item) => { 816 | item.add('languages', 'JS'); 817 | item.add('languages', 'JS'); 818 | item.add('languages', 'Ruby'); 819 | return item.save(); 820 | }).then((item) => { 821 | assert.deepEqual(item.get('languages'), ['JS', 'JS', 'Ruby']); 822 | item.remove('languages', 'JS'); 823 | return item.save(); 824 | }).then((item) => { 825 | assert.deepEqual(item.get('languages'), ['Ruby']); 826 | }) 827 | ); 828 | 829 | it('should saveAll and find 2 items', () => { 830 | const item = new Item(); 831 | item.set('price', 30); 832 | 833 | const item2 = new Item(); 834 | item2.set('price', 30); 835 | return Parse.Object.saveAll([item, item2]).then((items) => { 836 | assert.equal(items.length, 2); 837 | const query = new Parse.Query(Item); 838 | query.equalTo('price', 30); 839 | return query.find().then((finalItems) => { 840 | assert.equal(finalItems.length, 2); 841 | assert.equal(finalItems[0].get('price'), 30); 842 | assert.equal(finalItems[1].get('price'), 30); 843 | }); 844 | }); 845 | }); 846 | 847 | it('should find an item matching an or query', () => 848 | new Item() 849 | .set('price', 30) 850 | .save() 851 | .then(item => { 852 | const query = new Parse.Query(Item); 853 | query.equalTo('price', 30); 854 | 855 | const otherQuery = new Parse.Query(Item); 856 | otherQuery.equalTo('name', 'Chicken'); 857 | 858 | const orQuery = Parse.Query.or(query, otherQuery); 859 | return orQuery.find().then((items) => { 860 | assert.equal(items[0].id, item.id); 861 | }); 862 | }) 863 | ); 864 | 865 | it('should not find any items if they do not match an or query', () => 866 | new Item() 867 | .set('price', 30) 868 | .save() 869 | .then(() => { 870 | const query = new Parse.Query(Item); 871 | query.equalTo('price', 50); 872 | 873 | const otherQuery = new Parse.Query(Item); 874 | otherQuery.equalTo('name', 'Chicken'); 875 | 876 | const orQuery = Parse.Query.or(query, otherQuery); 877 | return orQuery.find().then((items) => { 878 | assert.equal(items.length, 0); 879 | }); 880 | }) 881 | ); 882 | 883 | it('should save 2 items and get one for a first() query', () => 884 | Promise.all([createItemP(30), createItemP(20)]).then(() => { 885 | const query = new Parse.Query(Item); 886 | return query.first().then((item) => { 887 | assert.equal(item.get('price'), 30); 888 | }); 889 | }) 890 | ); 891 | 892 | it('should handle nested includes', () => 893 | createBrandP('Acme') 894 | .then((newBrand) => 895 | createItemP(30, newBrand) 896 | .then((item) => { 897 | const brand = item.get('brand'); 898 | return createStoreWithItemP(item).then(() => { 899 | const query = new Parse.Query(Store); 900 | query.include('item'); 901 | query.include('item.brand'); 902 | return query.first().then((result) => { 903 | const resultItem = result.get('item'); 904 | const resultBrand = resultItem.get('brand'); 905 | assert.equal(resultItem.id, item.id); 906 | assert.equal(resultBrand.get('name'), 'Acme'); 907 | assert.equal(resultBrand.id, brand.id); 908 | }); 909 | }); 910 | }) 911 | ) 912 | ); 913 | 914 | it('should return invalid pointers if they are not included', () => { 915 | const item = new Item(); 916 | item.id = 'ZZZZZZZZ'; 917 | return createStoreWithItemP(item).then(() => { 918 | const query = new Parse.Query(Store); 919 | return query.first().then((result) => { 920 | assert.strictEqual(result.get('item').id, item.id); 921 | }); 922 | }); 923 | }); 924 | 925 | it('should leave includes of invalid pointers undefined', () => { 926 | const item = new Item(); 927 | item.id = 'ZZZZZZZZ'; 928 | return createStoreWithItemP(item).then(() => { 929 | const query = new Parse.Query(Store); 930 | query.include('item'); 931 | query.include('item.brand'); 932 | return query.first().then((result) => { 933 | assert.strictEqual(result.get('item'), undefined); 934 | }); 935 | }); 936 | }); 937 | 938 | it('should handle multiple nested includes', () => { 939 | let a1; 940 | let a2; 941 | let b; 942 | let c; 943 | 944 | return Promise.all([ 945 | new Parse.Object('a', { value: '1' }).save(), 946 | new Parse.Object('a', { value: '2' }).save(), 947 | ]) 948 | .then(([savedA1, savedA2]) => { 949 | a1 = savedA1; 950 | a2 = savedA2; 951 | return new Parse.Object('b', { a1, a2 }).save(); 952 | }) 953 | .then((savedB) => { 954 | b = savedB; 955 | return new Parse.Object('c', { b }).save(); 956 | }) 957 | .then((savedC) => { 958 | c = savedC; 959 | return new Parse.Query('c') 960 | .include('b') 961 | .include('b.a1') 962 | .include('b.a2') 963 | .first(); 964 | }) 965 | .then((loadedC) => { 966 | assert.equal(loadedC.id, c.id); 967 | assert.equal(loadedC.get('b').id, b.id); 968 | assert.equal(loadedC.get('b').get('a1').id, a1.id); 969 | assert.equal(loadedC.get('b').get('a2').id, a2.id); 970 | assert.equal(loadedC.get('b').get('a1').get('value'), a1.get('value')); 971 | assert.equal(loadedC.get('b').get('a2').get('value'), a2.get('value')); 972 | }); 973 | }); 974 | 975 | it('should handle includes over arrays of pointers', () => { 976 | const item1 = new Item({ cool: true }); 977 | const item2 = new Item({ cool: false }); 978 | const items = [item1, item2]; 979 | return Parse.Object.saveAll(items).then(() => { 980 | const brand = new Brand({ 981 | items, 982 | }); 983 | return brand.save(); 984 | }).then(() => { 985 | const q = new Parse.Query(Brand).include('items'); 986 | return q.first(); 987 | }).then((brand) => { 988 | assert(brand.get('items')[0].get('cool')); 989 | assert(!brand.get('items')[1].get('cool')); 990 | }); 991 | }); 992 | 993 | it('should handle nested includes over arrays of pointers', () => { 994 | const store = new Store({ location: 'SF' }); 995 | const item1 = new Item({ cool: true, store }); 996 | const item2 = new Item({ cool: false }); 997 | const items = [item1, item2]; 998 | return Parse.Object.saveAll(items.concat([store])).then(() => { 999 | const brand = new Brand({ 1000 | items, 1001 | }); 1002 | return brand.save(); 1003 | }).then(() => { 1004 | const q = new Parse.Query(Brand).include('items,items.store'); 1005 | return q.first(); 1006 | }).then((brand) => { 1007 | assert.equal(brand.get('items')[0].get('store').get('location'), 'SF'); 1008 | assert(!brand.get('items')[1].get('cool')); 1009 | }); 1010 | }); 1011 | 1012 | it('should handle includes for array of string', () => { 1013 | const item = new Item({ alternateNames: ['item1', 'originalItem'] }); 1014 | return Parse.Object.saveAll([item]).then(() => { 1015 | const brand = new Brand({ 1016 | item, 1017 | }); 1018 | return brand.save(); 1019 | }).then(() => { 1020 | const q = new Parse.Query(Brand).include('item,item.alternateNames'); 1021 | return q.first(); 1022 | }).then((brand) => { 1023 | assert.deepEqual(brand.get('item').get('alternateNames'), ['item1', 'originalItem']); 1024 | }); 1025 | }); 1026 | 1027 | it('should handle includes where item is missing', () => { 1028 | const item = new Item({ cool: true }); 1029 | const brand1 = new Brand({}); 1030 | const brand2 = new Brand({ item }); 1031 | return Parse.Object.saveAll([item, brand1, brand2]).then(() => { 1032 | const q = new Parse.Query(Brand).include('item'); 1033 | return q.find(); 1034 | }).then((brands) => { 1035 | assert(!brands[0].has('item')); 1036 | assert(brands[1].has('item')); 1037 | }); 1038 | }); 1039 | 1040 | it('should handle includes where nested array item is missing', () => { 1041 | const store = new Store({ location: 'SF' }); 1042 | const item1 = new Item({ cool: true, store }); 1043 | const item2 = new Item({ cool: false }); 1044 | const items = [item1, item2]; 1045 | return Parse.Object.saveAll(items.concat([store])).then(() => { 1046 | const brand = new Brand({ 1047 | items, 1048 | }); 1049 | return brand.save(); 1050 | }).then(() => { 1051 | const q = new Parse.Query(Brand).include('items,items.blah,wow'); 1052 | return q.first(); 1053 | }).then((brand) => { 1054 | assert(brand.get('items')[0].get('cool')); 1055 | assert(!brand.get('items')[1].get('cool')); 1056 | }); 1057 | }); 1058 | 1059 | it('should handle delete', () => { 1060 | const item = new Item(); 1061 | return item.save().then(() => new Parse.Query(Item).first() 1062 | ).then((foundItem) => { 1063 | assert(foundItem); 1064 | return foundItem.destroy(); 1065 | }).then(() => new Parse.Query(Item).first()) 1066 | .then((foundItem) => { 1067 | assert(!foundItem); 1068 | }); 1069 | }); 1070 | 1071 | it('should do a fetch query', () => { 1072 | let savedItem; 1073 | return new Item().save({ price: 30 }).then((item1) => { 1074 | savedItem = item1; 1075 | return Item.createWithoutData(item1.id).fetch(); 1076 | }).then((fetched) => { 1077 | assert.equal(fetched.id, savedItem.id); 1078 | assert.equal(fetched.get('price'), 30); 1079 | }); 1080 | }); 1081 | 1082 | it('should find with objectId', () => { 1083 | let savedItem; 1084 | return new Item().save({ price: 30 }).then((item1) => { 1085 | savedItem = item1; 1086 | return new Parse.Query(Item).equalTo('objectId', item1.id).first(); 1087 | }).then((fetched) => { 1088 | assert.equal(fetched.id, savedItem.id); 1089 | assert.equal(fetched.get('price'), 30); 1090 | }); 1091 | }); 1092 | 1093 | it('should get objectId', () => { 1094 | let savedItem; 1095 | return new Item().save({ price: 30 }).then((item1) => { 1096 | savedItem = item1; 1097 | return new Parse.Query(Item).get(item1.id); 1098 | }).then((fetched) => { 1099 | assert.equal(fetched.id, savedItem.id); 1100 | assert.equal(fetched.get('price'), 30); 1101 | }); 1102 | }); 1103 | 1104 | it('should find with objectId and where', () => 1105 | Promise.all([ 1106 | new Item().save({ price: 30 }), 1107 | new Item().save({ name: 'Device' }), 1108 | ]).then(([item1]) => { 1109 | const itemQuery = new Parse.Query(Item); 1110 | itemQuery.exists('nonExistent'); 1111 | itemQuery.equalTo('objectId', item1.id); 1112 | return itemQuery.find().then((items) => { 1113 | assert.equal(items.length, 0); 1114 | }); 1115 | }) 1116 | ); 1117 | 1118 | it('should match a correct when exists query', () => 1119 | Promise.all([ 1120 | new Item().save({ price: 30 }), 1121 | new Item().save({ name: 'Device' }), 1122 | ]).then(([item1]) => { 1123 | const itemQuery = new Parse.Query(Item); 1124 | itemQuery.exists('price'); 1125 | return itemQuery.find().then((items) => { 1126 | assert.equal(items.length, 1); 1127 | assert.equal(items[0].id, item1.id); 1128 | }); 1129 | }) 1130 | ); 1131 | 1132 | it('should match a correct when doesNotExist query', () => 1133 | Promise.all([ 1134 | new Item().save({ price: 30 }), 1135 | new Item().save({ name: 'Device' }), 1136 | ]).then(([item1, item2]) => { // eslint-disable-line no-unused-vars 1137 | const itemQuery = new Parse.Query(Item); 1138 | itemQuery.doesNotExist('price'); 1139 | return itemQuery.find().then((items) => { 1140 | assert.equal(items.length, 1); 1141 | assert.equal(items[0].id, item2.id); 1142 | }); 1143 | }) 1144 | ); 1145 | 1146 | it('should match a correct equalTo query for an object', () => 1147 | createItemP(30).then((item) => { 1148 | const store = new Store(); 1149 | store.set('item', item); 1150 | return store.save().then((savedStore) => { 1151 | const query = new Parse.Query(Store); 1152 | query.equalTo('item', item); 1153 | return query.find().then((results) => { 1154 | assert.equal(results[0].id, savedStore.id); 1155 | }); 1156 | }); 1157 | }) 1158 | ); 1159 | 1160 | it('should handle an equalTo null query for an object without a null field', () => 1161 | createItemP(30).then((item) => { 1162 | const store = new Store(); 1163 | store.set('item', item); 1164 | return store.save().then(() => { 1165 | const query = new Parse.Query(Store); 1166 | query.equalTo('item', null); 1167 | return query.find().then((results) => { 1168 | assert.equal(results.length, 0); 1169 | }); 1170 | }); 1171 | }) 1172 | ); 1173 | 1174 | it('should handle an equalTo null query for an object with a null field', () => { 1175 | const store = new Store(); 1176 | return store.save().then((savedStore) => { 1177 | const query = new Parse.Query(Store); 1178 | query.equalTo('item', null); 1179 | return query.find().then((results) => { 1180 | assert.equal(results[0].id, savedStore.id); 1181 | }); 1182 | }); 1183 | }); 1184 | 1185 | it('should handle a notEqualTo null query for an object without a null field', () => 1186 | createItemP(30).then((item) => { 1187 | const store = new Store(); 1188 | store.set('item', item); 1189 | return store.save().then((savedStore) => { 1190 | const query = new Parse.Query(Store); 1191 | query.notEqualTo('item', null); 1192 | return query.find().then((results) => { 1193 | assert.equal(results[0].id, savedStore.id); 1194 | }); 1195 | }); 1196 | }) 1197 | ); 1198 | 1199 | it('should handle a notEqualTo null query for an object with a null field', () => { 1200 | const store = new Store(); 1201 | return store.save().then(() => { 1202 | const query = new Parse.Query(Store); 1203 | query.notEqualTo('item', null); 1204 | return query.find().then((results) => { 1205 | assert.equal(results.length, 0); 1206 | }); 1207 | }); 1208 | }); 1209 | 1210 | it('should not match an incorrect equalTo query on price', () => 1211 | createItemP(30).then(() => 1212 | itemQueryP(20).then((results) => { 1213 | assert.equal(results.length, 0); 1214 | }) 1215 | ) 1216 | ); 1217 | 1218 | it('should not match an incorrect equalTo query on price and name', () => 1219 | createItemP(30).then(() => { 1220 | const query = new Parse.Query(Item); 1221 | query.equalTo('price', 30); 1222 | query.equalTo('name', 'pants'); 1223 | return query.find().then((results) => { 1224 | assert.equal(results.length, 0); 1225 | }); 1226 | }) 1227 | ); 1228 | 1229 | it('should match a containedIn query', () => 1230 | createItemP(30).then(() => { 1231 | const query = new Parse.Query(Item); 1232 | query.containedIn('price', [40, 30, 90]); 1233 | return query.find().then((results) => { 1234 | assert.equal(results.length, 1); 1235 | }); 1236 | }) 1237 | ); 1238 | 1239 | it('should not match an incorrect containedIn query', () => 1240 | createItemP(30).then(() => { 1241 | const query = new Parse.Query(Item); 1242 | query.containedIn('price', [40, 90]); 1243 | return query.find().then((results) => { 1244 | assert.equal(results.length, 0); 1245 | }); 1246 | }) 1247 | ); 1248 | 1249 | it('should match a containedIn query on array of items', () => 1250 | createItemP(30, 'Cereal', { languages: ['ruby', 'js', 'python'] }).then(() => { 1251 | const query = new Parse.Query(Item); 1252 | query.containedIn('languages', ['ruby']); 1253 | return query.find().then((results) => { 1254 | assert.equal(results.length, 1); 1255 | }); 1256 | }) 1257 | ); 1258 | 1259 | it('should find 2 objects when there are 2 matches', () => 1260 | Promise.all([createItemP(20), createItemP(20)]).then(() => { 1261 | const query = new Parse.Query(Item); 1262 | query.equalTo('price', 20); 1263 | return query.find().then((results) => { 1264 | assert.equal(results.length, 2); 1265 | }); 1266 | }) 1267 | ); 1268 | 1269 | it('should first() 1 object when there are 2 matches', () => 1270 | Promise.all([createItemP(20), createItemP(20)]).then(([item1]) => { 1271 | const query = new Parse.Query(Item); 1272 | query.equalTo('price', 20); 1273 | return query.first().then((result) => { 1274 | assert.equal(result.id, item1.id); 1275 | }); 1276 | }) 1277 | ); 1278 | 1279 | it('should match a query with 1 objects when 2 objects are present', () => 1280 | Promise.all([createItemP(20), createItemP(30)]).then(() => { 1281 | const query = new Parse.Query(Item); 1282 | query.equalTo('price', 20); 1283 | return query.find().then((results) => { 1284 | assert.equal(results.length, 1); 1285 | }); 1286 | }) 1287 | ); 1288 | 1289 | it('should match a date', () => { 1290 | const bornOnDate = new Date(); 1291 | const item = new Item({ bornOnDate }); 1292 | 1293 | return item.save().then(() => { 1294 | const query = new Parse.Query(Item); 1295 | query.equalTo('bornOnDate', bornOnDate); 1296 | return query.first().then((result) => { 1297 | assert(result.get('bornOnDate', bornOnDate)); 1298 | }); 1299 | }); 1300 | }); 1301 | 1302 | it('should properly handle date in query operator', () => { 1303 | const bornOnDate = new Date(); 1304 | const middleDate = new Date(); 1305 | const expireDate = new Date(); 1306 | middleDate.setDate(bornOnDate.getDate() + 1); 1307 | expireDate.setDate(bornOnDate.getDate() + 2); 1308 | 1309 | const item = new Item({ 1310 | bornOnDate, 1311 | expireDate, 1312 | }); 1313 | 1314 | return item.save().then(() => { 1315 | const query = new Parse.Query(Item); 1316 | query.lessThan('bornOnDate', middleDate); 1317 | query.greaterThan('expireDate', middleDate); 1318 | return query.first().then((result) => { 1319 | assert(result); 1320 | }); 1321 | }); 1322 | }); 1323 | 1324 | it('should handle $nin', () => 1325 | Promise.all([createItemP(20), createItemP(30)]).then(() => { 1326 | const query = new Parse.Query(Item); 1327 | query.notContainedIn('price', [30]); 1328 | return query.find(); 1329 | }).then((results) => { 1330 | assert.equal(results.length, 1); 1331 | assert.equal(results[0].get('price'), 20); 1332 | }) 1333 | ); 1334 | 1335 | it('should handle $nin on array field', () => { 1336 | const item1 = createItemP(20, 'crap', { languages: ['ruby', 'js', 'python'] }); 1337 | const item2 = createItemP(30, 'crap', { languages: ['ruby', 'js'] }); 1338 | Promise.all([item1, item2]).then(() => { 1339 | const query = new Parse.Query(Item); 1340 | query.notContainedIn('languages', ['python']); 1341 | return query.find(); 1342 | }).then((results) => { 1343 | assert.equal(results.length, 1); 1344 | assert.equal(results[0].get('price'), 30); 1345 | }); 1346 | }); 1347 | 1348 | it('should handle $nin on objectId', () => 1349 | createItemP(30).then((item) => { 1350 | const query = new Parse.Query(Item); 1351 | query.notContainedIn('objectId', [item.id]); 1352 | return query.find(); 1353 | }).then((results) => { 1354 | assert.equal(results.length, 0); 1355 | }) 1356 | ); 1357 | 1358 | it('should handle $nin with an empty array', () => 1359 | createItemP(30).then(() => { 1360 | const query = new Parse.Query(Item); 1361 | query.notContainedIn('objectId', []); 1362 | return query.find(); 1363 | }).then((results) => { 1364 | assert.equal(results.length, 1); 1365 | }) 1366 | ); 1367 | 1368 | it('should handle startsWith queries', () => 1369 | createBrandP('Acme').then(() => { 1370 | const query = new Parse.Query(Brand); 1371 | query.startsWith('name', 'Ac'); 1372 | return query.find(); 1373 | }).then((results) => { 1374 | assert.equal(results.length, 1); 1375 | }) 1376 | ); 1377 | 1378 | it('should handle matches queries', () => 1379 | createBrandP('Acme').then(() => { 1380 | const query = new Parse.Query(Brand); 1381 | query.matches('name', /ac/i); 1382 | return query.find(); 1383 | }).then((results) => { 1384 | assert.equal(results.length, 1); 1385 | }) 1386 | ); 1387 | 1388 | it('should handle matches queries that dont match', () => 1389 | createBrandP('Acme').then(() => { 1390 | const query = new Parse.Query(Brand); 1391 | query.matches('name', /ac/); 1392 | return query.find(); 1393 | }).then((results) => { 1394 | assert.equal(results.length, 0); 1395 | }) 1396 | ); 1397 | 1398 | it('should not overwrite included objects after a save', () => 1399 | createBrandP('Acme').then((brand) => 1400 | createItemP(30, brand).then((item) => 1401 | createStoreWithItemP(item).then(() => { 1402 | const query = new Parse.Query(Store); 1403 | query.include('item'); 1404 | query.include('item.brand'); 1405 | return query.first().then((str) => { 1406 | str.set('lol', 'wut'); 1407 | return str.save().then(() => { 1408 | assert.equal(str.get('item').get('brand').get('name'), brand.get('name')); 1409 | }); 1410 | }); 1411 | }) 1412 | ) 1413 | ) 1414 | ); 1415 | 1416 | it('should update an existing object correctly', () => 1417 | Promise.all([createItemP(30), createItemP(20)]).then(([item1, item2]) => 1418 | createStoreWithItemP(item1).then((store) => { 1419 | item2.set('price', 10); 1420 | store.set('item', item2); 1421 | return store.save().then((returnedStore) => { 1422 | assert(returnedStore.has('item')); 1423 | assert.equal(returnedStore.get('item').get('price'), 10); 1424 | assert(returnedStore.get('updatedAt') instanceof Date); 1425 | }); 1426 | }) 1427 | ) 1428 | ); 1429 | 1430 | it('should support a nested query', () => { 1431 | const brand0 = new Brand(); 1432 | brand0.set('name', 'Acme'); 1433 | brand0.set('country', 'US'); 1434 | return brand0.save().then((brand) => { 1435 | const item = new Item(); 1436 | item.set('price', 30); 1437 | item.set('country_code', 'US'); 1438 | item.set('state', 'CA'); 1439 | item.set('brand', brand); 1440 | return item.save(); 1441 | }).then(() => { 1442 | const store = new Store(); 1443 | store.set('state', 'CA'); 1444 | return store.save(); 1445 | }).then((store) => { 1446 | const brandQuery = new Parse.Query(Brand); 1447 | brandQuery.equalTo('name', 'Acme'); 1448 | 1449 | const itemQuery = new Parse.Query(Item); 1450 | itemQuery.matchesKeyInQuery('country_code', 'country', brandQuery); 1451 | 1452 | const storeQuery = new Parse.Query(Store); 1453 | storeQuery.matchesKeyInQuery('state', 'state', itemQuery); 1454 | return Promise.all([storeQuery.find(), Promise.resolve(store)]); 1455 | }) 1456 | .then(([storeMatches, store]) => { 1457 | assert.equal(storeMatches.length, 1); 1458 | assert.equal(storeMatches[0].id, store.id); 1459 | }); 1460 | }); 1461 | 1462 | it('should find items not filtered by a notContainedIn', () => 1463 | createItemP(30).then(() => { 1464 | const query = new Parse.Query(Item); 1465 | query.equalTo('price', 30); 1466 | query.notContainedIn('objectId', [234]); 1467 | return query.find().then((items) => { 1468 | assert.equal(items.length, 1); 1469 | }); 1470 | }) 1471 | ); 1472 | 1473 | it('should find not items filtered by a notContainedIn', () => 1474 | createItemP(30).then((item) => { 1475 | const query = new Parse.Query(Item); 1476 | query.equalTo('price', 30); 1477 | query.notContainedIn('objectId', [item.id]); 1478 | return query.find().then((items) => { 1479 | assert.equal(items.length, 0); 1480 | }); 1481 | }) 1482 | ); 1483 | 1484 | it('should handle a lessThan query', () => 1485 | createItemP(30).then(() => { 1486 | const query = new Parse.Query(Item); 1487 | query.lessThan('createdAt', new Date('2024-01-01T23:28:56.782Z')); 1488 | return query.find().then((items) => { 1489 | assert.equal(items.length, 1); 1490 | const newQuery = new Parse.Query(Item); 1491 | newQuery.greaterThan('createdAt', new Date()); 1492 | return newQuery.find().then((moreItems) => { 1493 | assert.equal(moreItems.length, 0); 1494 | }); 1495 | }); 1496 | }) 1497 | ); 1498 | 1499 | it('should handle a lessThanOrEqualTo query', () => 1500 | createItemP(30).then(() => { 1501 | const query = new Parse.Query(Item); 1502 | query.lessThanOrEqualTo('price', 30); 1503 | return query.find().then((items) => { 1504 | assert.equal(items.length, 1); 1505 | query.lessThanOrEqualTo('price', 20); 1506 | return query.find().then((moreItems) => { 1507 | assert.equal(moreItems.length, 0); 1508 | }); 1509 | }); 1510 | }) 1511 | ); 1512 | 1513 | it('should handle a greaterThan query', () => 1514 | createItemP(30).then(() => { 1515 | const query = new Parse.Query(Item); 1516 | query.greaterThan('price', 20); 1517 | return query.find().then((items) => { 1518 | assert.equal(items.length, 1); 1519 | query.greaterThan('price', 50); 1520 | return query.find().then((moreItems) => { 1521 | assert.equal(moreItems.length, 0); 1522 | }); 1523 | }); 1524 | }) 1525 | ); 1526 | 1527 | it('should handle a greaterThanOrEqualTo query', () => 1528 | createItemP(30).then(() => { 1529 | const query = new Parse.Query(Item); 1530 | query.greaterThanOrEqualTo('price', 30); 1531 | return query.find().then((items) => { 1532 | assert.equal(items.length, 1); 1533 | query.greaterThanOrEqualTo('price', 50); 1534 | return query.find().then((moreItems) => { 1535 | assert.equal(moreItems.length, 0); 1536 | }); 1537 | }); 1538 | }) 1539 | ); 1540 | 1541 | it('should handle multiple conditions for a single key', () => 1542 | createItemP(30).then(() => { 1543 | const query = new Parse.Query(Item); 1544 | query.greaterThan('price', 20); 1545 | query.lessThan('price', 40); 1546 | return query.find().then((items) => { 1547 | assert.equal(items.length, 1); 1548 | query.greaterThan('price', 30); 1549 | return query.find().then((moreItems) => { 1550 | assert.equal(moreItems.length, 0); 1551 | }); 1552 | }); 1553 | }) 1554 | ); 1555 | 1556 | it('should correctly handle matchesQuery', () => 1557 | createBrandP('Acme').then((brand) => 1558 | createItemP(30, brand).then((item) => 1559 | createStoreWithItemP(item).then(() => { 1560 | const brandQuery = new Parse.Query(Brand); 1561 | brandQuery.equalTo('name', 'Acme'); 1562 | 1563 | const itemQuery = new Parse.Query(Item); 1564 | itemQuery.matchesQuery('brand', brandQuery); 1565 | 1566 | const storeQuery = new Parse.Query(Store); 1567 | storeQuery.matchesQuery('item', itemQuery); 1568 | 1569 | return storeQuery.find().then((store) => { 1570 | assert(store); 1571 | }); 1572 | }) 1573 | ) 1574 | ) 1575 | ); 1576 | 1577 | it('should correctly count items in a matchesQuery', () => 1578 | createBrandP('Acme').then((brand) => 1579 | createItemP(30, brand).then((item) => 1580 | createStoreWithItemP(item).then(() => { 1581 | const itemQuery = new Parse.Query(Item); 1582 | itemQuery.equalTo('price', 30); 1583 | 1584 | const storeQuery = new Parse.Query(Store); 1585 | storeQuery.matchesQuery('item', itemQuery); 1586 | return storeQuery.count().then((storeCount) => { 1587 | assert.equal(storeCount, 1); 1588 | }); 1589 | }) 1590 | ) 1591 | ) 1592 | ); 1593 | 1594 | it('should skip and limit items appropriately', () => 1595 | createBrandP('Acme').then(() => 1596 | createBrandP('Acme 2').then(() => { 1597 | const brandQuery = new Parse.Query(Brand); 1598 | brandQuery.limit(1); 1599 | return brandQuery.find().then((brands) => { 1600 | assert.equal(brands.length, 1); 1601 | const brandQuery2 = new Parse.Query(Brand); 1602 | brandQuery2.limit(1); 1603 | brandQuery2.skip(1); 1604 | return brandQuery2.find().then((moreBrands) => { 1605 | assert.equal(moreBrands.length, 1); 1606 | assert.notEqual(moreBrands[0].id, brands[0].id); 1607 | }); 1608 | }); 1609 | }) 1610 | ) 1611 | ); 1612 | 1613 | it('should deep save and update nested objects', () => { 1614 | const brand = new Brand(); 1615 | brand.set('name', 'Acme'); 1616 | brand.set('country', 'US'); 1617 | const item = new Item(); 1618 | item.set('price', 30); 1619 | item.set('country_code', 'US'); 1620 | brand.set('items', [item]); 1621 | return brand.save().then((savedBrand) => { 1622 | assert.equal(savedBrand.get('items')[0].get('price'), item.get('price')); 1623 | 1624 | const item2 = new Item(); 1625 | item2.set('price', 20); 1626 | brand.set('items', [item2]); 1627 | return brand.save().then((updatedBrand) => { 1628 | assert.equal(updatedBrand.get('items')[0].get('price'), 20); 1629 | }); 1630 | }); 1631 | }); 1632 | 1633 | 1634 | context('when object has beforeSave hook registered', () => { 1635 | behavesLikeParseObjectOnBeforeSave('Brand', Brand); 1636 | }); 1637 | 1638 | context('when object has beforeDelete hook registered', () => { 1639 | behavesLikeParseObjectOnBeforeDelete('Brand', Brand); 1640 | }); 1641 | 1642 | context('when object has afterSave hook registered', () => { 1643 | behavesLikeParseObjectOnAfterSave('Brand', Brand); 1644 | }); 1645 | 1646 | it('successfully uses containsAll query', () => 1647 | Promise.all([createItemP(30), createItemP(20)]).then(([item1, item2]) => { 1648 | const store = new Store({ 1649 | items: [item1.toPointer(), item2.toPointer()], 1650 | }); 1651 | return store.save().then(() => { 1652 | const query = new Parse.Query(Store); 1653 | query.containsAll('items', [item1.toPointer(), item2.toPointer()]); 1654 | return query.find(); 1655 | }).then(stores => { 1656 | assert.equal(stores.length, 1); 1657 | const query = new Parse.Query(Store); 1658 | query.containsAll('items', [item2.toPointer(), 4]); 1659 | return query.find(); 1660 | }).then(stores => { 1661 | assert.equal(stores.length, 0); 1662 | }); 1663 | }) 1664 | ); 1665 | 1666 | it('should handle relations', () => { 1667 | const store = new Store(); 1668 | 1669 | const paperTowels0 = createItemP(20, 'paper towels'); 1670 | const toothPaste0 = createItemP(30, 'tooth paste'); 1671 | const toothBrush0 = createItemP(50, 'tooth brush'); 1672 | 1673 | return Promise.all([ 1674 | paperTowels0, 1675 | toothPaste0, 1676 | toothBrush0, 1677 | ]).then(([paperTowels, toothPaste]) => { 1678 | const relation = store.relation('items'); 1679 | relation.add(paperTowels); 1680 | relation.add(toothPaste); 1681 | return store.save(); 1682 | }) 1683 | .then(() => store.fetch() 1684 | ).then((fetchedStore) => { 1685 | const fetchRelation = fetchedStore.relation('items'); 1686 | return fetchRelation.query().count(); 1687 | }) 1688 | .then((itemCount) => { 1689 | assert.equal(itemCount, 2); 1690 | const relation = store.relation('items'); 1691 | const query = relation.query(); 1692 | return query.find(); 1693 | }) 1694 | .then((items) => { 1695 | assert.equal(items.length, 2); 1696 | assert.equal(items[0].className, 'Item'); 1697 | const relation = store.relation('items'); 1698 | relation.remove(items[1]); 1699 | return store.save(); 1700 | }) 1701 | .then((store1) => { 1702 | store1.relation('items'); 1703 | return store.relation('items').query().find(); 1704 | }) 1705 | .then((items) => { 1706 | assert.equal(items.length, 1); 1707 | }); 1708 | }); 1709 | 1710 | it('should not have duplicates in relations', () => 1711 | new Item().save().then(i => 1712 | new Brand() 1713 | .save() 1714 | .then(b => { 1715 | b.relation('items').add(i); 1716 | b.relation('items').add(i); 1717 | return b.save(); 1718 | }) 1719 | .then(b => b.fetch()) 1720 | .then(b => { 1721 | b.relation('items').add(i); 1722 | return b.save(); 1723 | }) 1724 | .then(b => b.fetch()) 1725 | .then(b => b.relation('items').query().find()) 1726 | .then(items => { 1727 | assert.equal(items.length, 1); 1728 | assert.equal(items[0].id, i.id); 1729 | }) 1730 | ) 1731 | ); 1732 | 1733 | it('should handle a direct query on a relation field', () => { 1734 | const store = new Store({ name: 'store 1' }); 1735 | const store2 = new Store({ name: 'store 2' }); 1736 | let tpId; 1737 | 1738 | const paperTowels0 = createItemP(20, 'paper towels'); 1739 | const toothPaste0 = createItemP(30, 'tooth paste'); 1740 | const toothBrush0 = createItemP(50, 'tooth brush'); 1741 | return Promise.all([ 1742 | paperTowels0, 1743 | toothPaste0, 1744 | toothBrush0, 1745 | store, 1746 | store2, 1747 | ]).then(([paperTowels, toothPaste]) => { 1748 | tpId = toothPaste.id; 1749 | const relation = store2.relation('items'); 1750 | relation.add(paperTowels); 1751 | relation.add(toothPaste); 1752 | return store2.save(); 1753 | }).then(() => { 1754 | const query = new Parse.Query(Store); 1755 | query.equalTo('items', Item.createWithoutData(tpId)); 1756 | return query.find(); 1757 | }).then((results) => { 1758 | assert.equal(results.length, 1); 1759 | assert.equal(results[0].get('name'), 'store 2'); 1760 | }); 1761 | }); 1762 | 1763 | it('should handle the User class', () => { 1764 | const user = new Parse.User({ name: 'Turtle' }); 1765 | return user.save().then(() => new Parse.Query(Parse.User).find()) 1766 | .then((foundUsers) => { 1767 | assert.equal(foundUsers.length, 1); 1768 | assert.equal(foundUsers[0].get('name'), 'Turtle'); 1769 | }); 1770 | }); 1771 | 1772 | it('should handle the Role class', () => { 1773 | const roleACL = new Parse.ACL(); 1774 | roleACL.setPublicReadAccess(true); 1775 | const role = new Parse.Role('Turtle', roleACL); 1776 | return role.save().then(() => new Parse.Query(Parse.Role).find()) 1777 | .then((foundRoles) => { 1778 | assert.equal(foundRoles.length, 1); 1779 | assert.equal(foundRoles[0].get('name'), 'Turtle'); 1780 | }); 1781 | }); 1782 | 1783 | it('should handle redirectClassNameForKey', () => { 1784 | const user = new Parse.User({ name: 'T Rutlidge' }); 1785 | return user.save().then((savedUser) => { 1786 | const roleACL = new Parse.ACL(); 1787 | roleACL.setPublicReadAccess(true); 1788 | 1789 | const role = new Parse.Role('Turtle', roleACL); 1790 | role.getUsers().add(savedUser); 1791 | return role.save(); 1792 | }).then(() => new Parse.Query(Parse.Role).equalTo('name', 'Turtle').first()) 1793 | .then((foundRole) => foundRole.getUsers().query().find()) 1794 | .then((foundUsers) => { 1795 | assert.equal(foundUsers.length, 1); 1796 | assert.equal(foundUsers[0].get('name'), 'T Rutlidge'); 1797 | }); 1798 | }); 1799 | 1800 | it('should correctly find nested object in a where query', () => { 1801 | const store = new Store({ 1802 | name: 'store 1', 1803 | customOptions: { 1804 | isOpenHolidays: true, 1805 | weekendAvailability: { 1806 | sat: true, 1807 | sun: false, 1808 | }, 1809 | }, 1810 | }); 1811 | return store.save().then(() => { 1812 | let storeQuery = new Parse.Query(Store); 1813 | storeQuery.equalTo('customOptions.isOpenHolidays', true); 1814 | return storeQuery.count().then((storeCount) => { 1815 | assert.equal(storeCount, 1); 1816 | storeQuery = new Parse.Query(Store); 1817 | storeQuery.equalTo('customOptions.blah', true); 1818 | return storeQuery.count(); 1819 | }).then((count) => { 1820 | assert.equal(count, 0); 1821 | storeQuery = new Parse.Query(Store); 1822 | storeQuery.equalTo('customOptions.weekendAvailability.sun', false); 1823 | return storeQuery.count(); 1824 | }).then((count) => { 1825 | assert.equal(count, 1); 1826 | storeQuery = new Parse.Query(Store); 1827 | storeQuery.equalTo('customOptions.weekendAvailability.sun', true); 1828 | return storeQuery.count(); 1829 | }) 1830 | .then((count) => { 1831 | assert.equal(count, 0); 1832 | }); 1833 | }); 1834 | }); 1835 | }); 1836 | --------------------------------------------------------------------------------