├── .babelrc ├── .gitignore ├── .travis.yml ├── LICENSE ├── README.md ├── demos ├── react-native │ └── ListViewDemo │ │ ├── .babelrc │ │ ├── .flowconfig │ │ ├── .gitignore │ │ ├── .watchmanconfig │ │ ├── README.md │ │ ├── index.ios.js │ │ ├── ios │ │ ├── ListViewDemo.xcodeproj │ │ │ ├── project.pbxproj │ │ │ └── xcshareddata │ │ │ │ └── xcschemes │ │ │ │ └── ListViewDemo.xcscheme │ │ ├── ListViewDemo │ │ │ ├── AppDelegate.h │ │ │ ├── AppDelegate.m │ │ │ ├── Base.lproj │ │ │ │ └── LaunchScreen.xib │ │ │ ├── Images.xcassets │ │ │ │ └── AppIcon.appiconset │ │ │ │ │ └── Contents.json │ │ │ ├── Info.plist │ │ │ └── main.m │ │ └── ListViewDemoTests │ │ │ ├── Info.plist │ │ │ └── ListViewDemoTests.m │ │ ├── package.json │ │ └── src │ │ ├── ItemList.js │ │ ├── ItemManager.js │ │ └── RemoteData.js └── react-redux │ ├── AnyBudget │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── components │ │ ├── App.react.js │ │ ├── AppWrapper.react.js │ │ ├── BarChart.react.js │ │ ├── Categories.js │ │ ├── DonutChart.react.js │ │ ├── ExpenseCreator.react.js │ │ ├── Expenses.react.js │ │ ├── LoginWrapper.react.js │ │ ├── Overview.react.js │ │ └── Sidebar.react.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── redux │ │ ├── Actions.js │ │ └── budgetReducer.js │ ├── styles.css │ └── webpack.config.js │ ├── README.md │ └── todo │ ├── .babelrc │ ├── .gitignore │ ├── README.md │ ├── components │ ├── App.react.js │ ├── PrettyDate.react.js │ ├── TodoCreator.react.js │ ├── TodoItem.react.js │ └── TodoList.react.js │ ├── index.html │ ├── index.js │ ├── package.json │ ├── redux │ ├── Actions.js │ └── todoReducer.js │ ├── styles.css │ └── webpack.config.js ├── index.js ├── integration ├── .gitignore ├── README.md ├── cloud │ └── main.js ├── package.json ├── server.js └── test │ ├── ArrayOperationsTest.js │ ├── CloudTest.js │ ├── DestroyTest.js │ ├── IncrementTest.js │ ├── QueryTest.js │ ├── SaveTest.js │ ├── UserTest.js │ └── clearApps.js ├── package.json ├── run_integration.sh ├── src ├── .flowconfig ├── App.js ├── Cloud.js ├── Date.js ├── Destroy.js ├── Installation.js ├── Ops.js ├── Query.js ├── Save.js ├── Types.js ├── User.js ├── WireFormat.js ├── arrayValueMatches.js ├── copyWithoutFields.js ├── deepCopy.js └── defs │ └── ibeam.js └── test ├── Ops-test.js ├── Query-test.js ├── WireFormat-test.js ├── arrayValueMatches-test.js └── deepCopy-test.js /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1"], 3 | "plugins": ["syntax-flow", "transform-flow-strip-types"] 4 | } 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | lib/ 3 | *.log 4 | .DS_Store 5 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - '5' 5 | 6 | branches: 7 | only: 8 | - master 9 | 10 | env: 11 | - CXX=g++-4.8 12 | 13 | services: mongodb 14 | 15 | addons: 16 | apt: 17 | sources: 18 | - mongodb-3.0-precise 19 | - ubuntu-toolchain-r-test 20 | packages: 21 | - mongodb-org-server 22 | - g++-4.8 23 | 24 | script: npm test && ./run_integration.sh 25 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | BSD 3-Clause License 2 | 3 | Copyright (c) 2016, Andrew Imm 4 | All rights reserved. 5 | 6 | Redistribution and use in source and binary forms, with or without 7 | modification, are permitted provided that the following conditions are met: 8 | 9 | * Redistributions of source code must retain the above copyright notice, this 10 | list of conditions and the following disclaimer. 11 | 12 | * Redistributions in binary form must reproduce the above copyright notice, 13 | this list of conditions and the following disclaimer in the documentation 14 | and/or other materials provided with the distribution. 15 | 16 | * Neither the name of the copyright holder nor the names of its 17 | contributors may be used to endorse or promote products derived from 18 | this software without specific prior written permission. 19 | 20 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 21 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 22 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 23 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 24 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 25 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 26 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 27 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 28 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 29 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 30 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Parse Lite - The universal JS library for Parse Server 2 | [![Build Status][build-status-svg]][build-status-link] 3 | [![License][license-svg]][license-link] 4 | 5 | Parse Lite is a lightweight SDK for 6 | [Parse Server](https://github.com/ParsePlatform/Parse-Server). It handles all of 7 | the complexities of authentication and interchange, leaving you with the 8 | freedom to build your app as you see fit. The best part? It's completely 9 | unopinionated about your environment. The same code will run the same way in the 10 | browser, in Node JS, or on React Native, because it does not produce any side 11 | effects in the global environment. Functionality like offline storage or user 12 | persistence is left for add-on packages, because we believe in a core SDK that 13 | provides straightforward server communication and does only what you ask of it. 14 | 15 | Parse Lite takes a functional approach to creating, modifying, and retrieving 16 | data from your Parse Server. Each query, mutation, and fetch generates a new 17 | immutable instance, so you can be sure that your application isn't plagued by 18 | unintended side effects. 19 | 20 | This is great for building server applications where keeping mutable objects 21 | in shared memory can have dangerous implications. Modifying an object ensures 22 | that the original copy remains unviolated in all other contexts where it might 23 | be used. 24 | 25 | This also makes it easy to build reactive applications using tools like React 26 | and Redux. If two objects are equal in memory, you know no change has occurred. 27 | If they're not equal, you know it's necessary to trigger an update or re-render 28 | part of the UI. [See a demo app](demos/react-redux/). 29 | 30 | ## Creating an object 31 | 32 | In this library, objects on your Parse server are implemented as simple 33 | key-value maps. This means creating an object is simple: 34 | 35 | ```js 36 | let player = { name: 'Andrew', score: 0 }; 37 | // That's it! 38 | ``` 39 | 40 | Want to reference an already-existing object? Include the `objectId` field. 41 | 42 | You'll notice that the class name is not a property of the object. That's 43 | intentional – the class name determines *where* to put the object, it is not a 44 | property of the object. You'll see how class names are used to direct saves 45 | and fetches later on. 46 | 47 | ## Modifying an object 48 | 49 | Objects are modified through `Ops`. These operations provide basic access to 50 | setting and unsetting properties, as well as atomic operations like incrementing 51 | or decrementing counts, or modification of array fields. 52 | 53 | ### Set operations 54 | 55 | Set operations establish the value of fields on your object. They overwrite any 56 | prior value. 57 | 58 | ```js 59 | import {Ops} from 'parse-lite'; 60 | 61 | // Assuming player was already created somewhere in the application 62 | let updatedPlayer = Ops.set(player, { verified: true }); 63 | // updatedPlayer.verified is now true 64 | ``` 65 | 66 | Earlier, we initialized an object with a few fields. For an unsaved object, this 67 | is the equivalent of setting those fields on an initially-empty object. 68 | 69 | ```js 70 | // The following two operations are the same: 71 | let player1 = { name: 'Andrew', score: 0 }; 72 | let player2 = Ops.set({}, { name: 'Andrew', score: 0 }); 73 | ``` 74 | 75 | ### Unset operations 76 | 77 | Want to remove a field from an object? `Ops.unset` will handle that. 78 | 79 | ```js 80 | // Remove the 'verified' field from player 81 | let updatedPlayer = Ops.unset(player, 'verified'); 82 | ``` 83 | 84 | ### Increment operations 85 | 86 | Numeric fields can be atomically incremented by any amount. When the request 87 | hits the server, the operation will be made on the current value of the 88 | database, allowing transactions to take place without knowing the initial value. 89 | 90 | `Ops.increment` takes the object to be updated, as well as the field that will 91 | be modified. It can take an optional third argument that specifies the numeric 92 | amount to increment by (negative and float values are supported). 93 | 94 | ```js 95 | // By default, increments the score by 1 96 | let plusOne = Ops.increment(player, 'score'); 97 | 98 | // Increments the score by 10 99 | let plusTen = Ops.increment(player, 'score', 10); 100 | 101 | // Decrements the score by 5 102 | let minusFive = Ops.increment(player, 'score', -5); 103 | 104 | // Increments the score by 0.5 105 | let plusHalf = Ops.increment(player, 'score', 0.5); 106 | ``` 107 | 108 | ### Add-to-array operations 109 | 110 | Array fields have a number of possible atomic operations. The first is 111 | `Ops.add`, which adds the provided value to the end of the array without needing 112 | to know its initial value. 113 | 114 | ```js 115 | // Add the string '#latergram' to the 'tags' field 116 | let updated = Ops.add(photo, 'tags', '#latergram'); 117 | ``` 118 | 119 | ### Remove-from-array operations 120 | 121 | Similarly, values can be atomically removed from array fields. `Ops.remove` will 122 | remove all instances of a value from an array field. 123 | 124 | ```js 125 | // Remove all instances of the string '#latergram' from the 'tags' field 126 | let updated = Ops.remove(photo, 'tags', '#latergram'); 127 | ``` 128 | 129 | ### Add-unique-to-array operations 130 | 131 | Sometimes you want to only add a value to an array if it's not already there. 132 | `Ops.addUnique` handles this behavior. 133 | 134 | ```js 135 | // Adds '#tbt' to the 'tags' field IF it's not already contained 136 | let updated = Ops.addUnique(photo, 'tags', '#tbt'); 137 | ``` 138 | 139 | ## Establishing a connection with a server 140 | 141 | All of these modifications are useless if we can't persist them to the server. 142 | Saves and queries require first establishing to a specific application and 143 | server. This is done by creating an `App`. 144 | 145 | `App` takes a number of options at creation. Not all of them may be necessary 146 | for your particular application. At the very least, you will need to include 147 | a Server URL and an Application ID. 148 | 149 | ```js 150 | import {App} from 'parse-lite'; 151 | 152 | let app = new App({ 153 | host: 'my.parse.server/path', // Required 154 | applicationId: 'MyAppId', // Required 155 | 156 | masterKey: 's3cret!', // Only for servers that need universal data access 157 | }); 158 | ``` 159 | 160 | This `App` object will be used to make all network requests to the server. 161 | Having multiple `App` objects lets you communicate with multiple Parse apps from 162 | the same program, if that's something you want to do. 163 | 164 | Under the hood, `App` uses [I-Beam](https://github.com/andrewimm/ibeam) to 165 | communicate with your server, and it supports all of the options that I-Beam 166 | Clients support. If you're using Parse Lite in a Node environment, you'll need 167 | to configure your `App` to use I-Beam's HTTP Controller, like so: 168 | 169 | ```js 170 | import {App} from 'parse-lite'; 171 | import HttpController from 'ibeam/http-node'; 172 | 173 | let app = new App({ 174 | host: 'my.parse.server/path', 175 | applicationId: 'MyAppId', 176 | httpController: HttpController, // <-- 177 | masterKey: 's3cret!', 178 | }); 179 | ``` 180 | 181 | This may change in the future, but for now Parse Lite embraces a philosophy of 182 | letting developers be explicit about exactly what they want. 183 | 184 | ## Saving an object 185 | 186 | The `Save` method allows you to store or update an object, as long as you 187 | provide information on where it's stored. This is an `App` object specifying the 188 | server data, and a class name determining which table the object is placed in. 189 | 190 | ```js 191 | // Creates an object with a 'count' field set to 5 192 | // Stores it in the 'MyClass' table associated with `app` 193 | Save(app, 'MyClass', { count: 5 }) 194 | ``` 195 | 196 | Saving is an asynchronous process, and calls to `Save` will return a JavaScript 197 | `Promise` that is resolved when the server responds. If the process was 198 | successful, the `Promise` will be resolved with an updated version of the object 199 | that was saved. If an error occurred, the `Promise` will be rejected. Because 200 | this library is focused on functional objects that don't share mutable state, 201 | the object returned is completely different from the object that was originally 202 | saved. Modifying one will not modify the other. This way, each object can 203 | represent the state of that data at different points in time. 204 | 205 | ```js 206 | let obj = Ops.Increment({}, 'count'); 207 | Save(app, 'MyClass', obj).then((result) => { 208 | // result is the saved version of obj 209 | // It has the latest server state of all modified fields 210 | console.log('The count is now', result.count); 211 | }, (err) => { 212 | console.log('An error occurred:', err); 213 | }); 214 | ``` 215 | 216 | ## Fetching objects from the server 217 | 218 | Once objects have been saved to the server, you'll want a way to retrieve them. 219 | The `Query` module provides functionality to fetch objects, either directly or 220 | through database queries. 221 | 222 | ### Getting a specific object 223 | 224 | Fetching an object by its `objectId` is simple, and can be done with the 225 | `Query.get` method. Provided an `App`, a class name, and an object id, `get` 226 | will return a `Promise` that is resolved with the object, should it be found. 227 | If an error occurs, or the object is not found, the `Promise` will be rejected 228 | with the error. 229 | 230 | ```js 231 | import {Query} from 'parse-lite'; 232 | 233 | // Fetch the Item with objectId 'abc123' 234 | Query.get(app, 'Item', 'abc123').then((result) => { 235 | // `result` is the object 236 | // Now you can do something with it! 237 | }, (err) => { 238 | console.log('An error occurred:', err); 239 | }); 240 | ``` 241 | 242 | ### Querying for multiple objects 243 | 244 | Queries are constructed in a similar method to object mutations. At a basic 245 | level, they are simply JSON payloads that implement the Parse Server query 246 | format. The `Query` module provides developer-friendly ways to create these 247 | objects and refine them. Just like an object mutation, each new query mutation 248 | generates a new query object, so that you can build off of queries in a 249 | non-destructive manner. 250 | 251 | `Query.find` takes an `App`, a class name, and a query representation object. 252 | It returns a `Promise` that is resolved with an array of objects matching the 253 | query constraints. If an error occurs, the `Promise` will be rejected with the 254 | error. 255 | 256 | ```js 257 | // fetch the first 10 objects from the Item class 258 | Query.find(app, 'Item', {limit: 10}).then((objects) => { 259 | // objects is an array of Item results 260 | }, (err) => { 261 | console.log('An error occurred:', err); 262 | }); 263 | ``` 264 | 265 | ### Querying with no filters or constraints 266 | 267 | The most basic query retrieves objects with no filtering, and the server's 268 | default constraints. With no options, this query can be represented as an empty 269 | JS Object. 270 | 271 | ```js 272 | let q = {}; // No constraints 273 | Query.find(app, 'Item', q); 274 | ``` 275 | 276 | You can also use a query object that has been initialized to the default values. 277 | This is constructed by calling `Query.emptyQuery()`. 278 | 279 | ### Finding objects with specific values 280 | 281 | `Query.equalTo` adds a constraint to fetch objects where a field matches a 282 | specific value. It takes a query object, a field, and the value to match. 283 | 284 | ```js 285 | // Filter for objects where 'flagged' equals true 286 | let q1 = Query.equalTo({}, 'flagged', true); 287 | // Also filter for objects where 'draft' equals false 288 | let q2 = Query.equalTo(q1, 'draft', false); 289 | ``` 290 | 291 | `equalTo` is also used to return rows where an array field contains a specific 292 | value. 293 | 294 | ```js 295 | // Filter for objects where 'tags' contains "hot" 296 | let q3 = Query.equalTo(q2, 'tags', 'hot'); 297 | ``` 298 | 299 | If you need to locate array fields than contain more than one specific value, 300 | you can use `Query.containsAll`. Similar to using `equalTo` on an array field, 301 | it takes a query object, a field, and an array of values to match. 302 | 303 | ```js 304 | // Fetch objects that have all three tags 305 | let q = Query.containsAll({}, 'tags', ['new', 'local', 'promoted']); 306 | ``` 307 | 308 | You can also locate objects where a field is *not* equal to a specific value. 309 | `Query.notEqualTo` takes similar arguments: a query object, a field, and the 310 | value you do not want to match. 311 | 312 | ```js 313 | // Filter where 'color' is not "red" 314 | let notRed = Query.notEqualTo({}, 'color', 'red'); 315 | ``` 316 | 317 | > Developer note: databases typically cannot optimize queries that look for 318 | > fields that are not equal to something. This type of query suggests that 319 | > you're matching nearly all possible values, which can involve scanning the 320 | > entire table. Use this type of query sparingly, and consider improving your 321 | > server performance by rewriting your query to avoid "not equals" expressions. 322 | 323 | ### Matching multiple values 324 | 325 | If you want to fetch objects that match anything in a set of values, you can 326 | do so without running multiple queries. `Query.containedIn` takes a query 327 | object, a field, and an array of values you want to match. 328 | 329 | ```js 330 | // Fetch only rows with primary colors 331 | let primary = Query.containedIn({}, 'color', ['red', 'yellow', 'blue']); 332 | ``` 333 | 334 | You can also fetch objects that don't match a group of values. 335 | `Query.notContainedIn` takes a query object, a field, and an array of values to 336 | not match. The same concerns about inequality performance exist for this query. 337 | 338 | ### Inequality queries 339 | 340 | Fields that are directly comparable – `number`s, `string`s, and `Date`s – can be 341 | fetched with inequality constraints. Queries support `lessThan`, 342 | `lessThanOrEqualTo`, `greaterThan`, and `greaterThanOrEqualTo` filters. Each one 343 | takes a query object, a field, and the value you want to compare to. 344 | 345 | ```js 346 | // Fetch all ratings between 2 and 4, inclusive 347 | let midrange = Query.greaterThanOrEqualTo({}, 'rating', 2); 348 | midrange = Query.lessThanOrEqualTo(midrange, 'rating', 4); 349 | ``` 350 | 351 | ### Fetching where fields are set (or unset) 352 | 353 | `Query.exists` and `Query.doesNotExist` allow matching objects where a field is 354 | set or unset. They both take a query object and a field. 355 | 356 | ```js 357 | // Fetch all players with a nickname 358 | let haveNick = Query.exists({}, 'nickname'); 359 | 360 | // Fetch all players without a nickname 361 | let noNick = Query.doesNotExist({}, 'nickname'); 362 | ``` 363 | 364 | ### String matching 365 | 366 | You can match fields that contain a specific substring with `Query.contains`, 367 | `Query.startsWith`, and `Query.endsWith`. Each takes a query object, a field, 368 | and the substring you want to match. 369 | 370 | ```js 371 | // Extract paths that begin with "https://" and end with ".js" 372 | let resources = Query.startsWith({}, 'path', 'https://'); 373 | resources = Query.endsWith(resources, 'path', '.js'); 374 | ``` 375 | 376 | ### Filtering with GeoPoints 377 | 378 | `GeoPoint` fields support querying by distance. You can search for points within 379 | a geographic rectangle, or within some radius of a single point. 380 | `Query.withinRadians` takes a query object, a field, a `GeoPoint` to begin 381 | searching from, and a maximum search distance (in radians). Similarly, you can 382 | filter using `Query.withinMiles` or `Query.withinKilometers`, which take the 383 | same arguments. 384 | 385 | ```js 386 | // Search for restaurants within 5 miles of userLocation 387 | let nearby = Query.withinMiles({}, 'location', userLocation, 5); 388 | ``` 389 | 390 | Parse Server also supports fetching objects contained within the rectangle 391 | formed by two `GeoPoint`s. `Query.withinGeoBox` takes a query object, a field, 392 | and two `GeoPoint`s representing the corners of the box to search within. 393 | 394 | ```js 395 | // Where southwest and northeast are two GeoPoints 396 | let withinBox = Query.withinGeoBox({}, 'location', southwest, northeast); 397 | ``` 398 | 399 | ## Destroying an object 400 | 401 | The `Destroy` method allows you to destroy an object on the server. Provided an 402 | `App`, a class name, and a reference to the object, `Destroy` will return a 403 | `Promise` that is resolved when the object is destroyed. If an error occurs, 404 | the `Promise` will be rejected with the error. 405 | 406 | ```js 407 | // Destroy the Item with objectId 'abc123' 408 | Destroy(app, 'Item', 'abc123').then((result) => { 409 | // the object was destroyed 410 | }, (err) => { 411 | console.log('An error occurred:', err); 412 | }); 413 | ``` 414 | 415 | Additionally, passing a local copy of the object will work. 416 | ```js 417 | let obj = { objectId: 'abc124' }; 418 | Destroy(app, 'Item', obj).then((result) => { 419 | // the object was destroyed 420 | }, (err) => { 421 | console.log('An error occurred:', err); 422 | }); 423 | ``` 424 | 425 | [build-status-svg]: https://travis-ci.org/andrewimm/parse-lite.svg?branch=master 426 | [build-status-link]: https://travis-ci.org/andrewimm/parse-lite 427 | [license-svg]: https://img.shields.io/badge/license-BSD-lightgrey.svg 428 | [license-link]: https://github.com/andrewimm/parse-lite/blob/master/LICENSE 429 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["react-native"] 3 | } 4 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | # We fork some components by platform. 4 | .*/*[.]android.js 5 | 6 | # Ignore templates with `@flow` in header 7 | .*/local-cli/generator.* 8 | 9 | # Ignore malformed json 10 | .*/node_modules/y18n/test/.*\.json 11 | 12 | # Ignore the website subdir 13 | /website/.* 14 | 15 | # Ignore BUCK generated dirs 16 | /\.buckd/ 17 | 18 | # Ignore unexpected extra @providesModule 19 | .*/node_modules/commoner/test/source/widget/share.js 20 | 21 | # Ignore duplicate module providers 22 | # For RN Apps installed via npm, "Libraries" folder is inside node_modules/react-native but in the source repo it is in the root 23 | .*/Libraries/react-native/React.js 24 | .*/Libraries/react-native/ReactNative.js 25 | .*/node_modules/jest-runtime/build/__tests__/.* 26 | 27 | [include] 28 | 29 | [libs] 30 | node_modules/react-native/Libraries/react-native/react-native-interface.js 31 | node_modules/react-native/flow 32 | flow/ 33 | 34 | [options] 35 | module.system=haste 36 | 37 | esproposal.class_static_fields=enable 38 | esproposal.class_instance_fields=enable 39 | 40 | experimental.strict_type_args=true 41 | 42 | munge_underscores=true 43 | 44 | module.name_mapper='^image![a-zA-Z0-9$_-]+$' -> 'GlobalImageStub' 45 | module.name_mapper='^[./a-zA-Z0-9$_-]+\.\(bmp\|gif\|jpg\|jpeg\|png\|psd\|svg\|webp\|m4v\|mov\|mp4\|mpeg\|mpg\|webm\|aac\|aiff\|caf\|m4a\|mp3\|wav\|html\|pdf\)$' -> 'RelativeImageStub' 46 | 47 | suppress_type=$FlowIssue 48 | suppress_type=$FlowFixMe 49 | suppress_type=$FixMe 50 | 51 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixMe\\($\\|[^(]\\|(\\(>=0\\.\\(30\\|[1-2][0-9]\\|[0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\) 52 | suppress_comment=\\(.\\|\n\\)*\\$FlowIssue\\((\\(>=0\\.\\(30\\|1[0-9]\\|[1-2][0-9]\\).[0-9]\\)? *\\(site=[a-z,_]*react_native[a-z,_]*\\)?)\\)?:? #[0-9]+ 53 | suppress_comment=\\(.\\|\n\\)*\\$FlowFixedInNextDeploy 54 | 55 | unsafe.enable_getters_and_setters=true 56 | 57 | [version] 58 | ^0.30.0 59 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/.gitignore: -------------------------------------------------------------------------------- 1 | # OSX 2 | # 3 | .DS_Store 4 | 5 | # Xcode 6 | # 7 | build/ 8 | *.pbxuser 9 | !default.pbxuser 10 | *.mode1v3 11 | !default.mode1v3 12 | *.mode2v3 13 | !default.mode2v3 14 | *.perspectivev3 15 | !default.perspectivev3 16 | xcuserdata 17 | *.xccheckout 18 | *.moved-aside 19 | DerivedData 20 | *.hmap 21 | *.ipa 22 | *.xcuserstate 23 | project.xcworkspace 24 | 25 | # Android/IJ 26 | # 27 | *.iml 28 | .idea 29 | .gradle 30 | local.properties 31 | 32 | # node.js 33 | # 34 | node_modules/ 35 | npm-debug.log 36 | 37 | # BUCK 38 | buck-out/ 39 | \.buckd/ 40 | android/app/libs 41 | android/keystores/debug.keystore 42 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/README.md: -------------------------------------------------------------------------------- 1 | # ListItemDemo: A Parse Lite + React Native Demo 2 | 3 | This demo uses Parse Lite to populate a ListView in React Native. 4 | 5 | ### Try it yourself 6 | 7 | You can set this app to point to your Parse Server, and run it yourself. 8 | 9 | 1. Run `npm install` from this directory to install the development dependencies. 10 | 11 | 2. Put your Parse Server info into `./src/RemoteData.js`. At the top, you'll find a line that initializes a new `App` object. Within its options parameter, set `host` to be the path to your Parse Server, and set `applicationId` to be the id of your app. 12 | ```js 13 | // INITIALIZE HERE 14 | const app = new App({ 15 | host: 'my.parse.server/path', 16 | applicationId: 'abc123', 17 | }); 18 | ``` 19 | 20 | 3. Open `./ios/ListViewDemo.xcodeproj` with XCode, and build the app. 21 | 22 | 4. Run the app in the Simulator. Your app should load up and point to your Parse Server. Any changes you make should be persisted when you refresh the app. 23 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/index.ios.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Parse Lite + React Native demo 3 | * https://github.com/facebook/react-native 4 | * @flow 5 | */ 6 | 7 | import React, { Component } from 'react'; 8 | import { 9 | AppRegistry, 10 | StyleSheet, 11 | Text, 12 | View 13 | } from 'react-native'; 14 | 15 | import RemoteData from './src/RemoteData'; 16 | 17 | class ListViewDemo extends Component { 18 | render() { 19 | return ( 20 | 21 | 22 | 23 | ); 24 | } 25 | } 26 | 27 | const styles = StyleSheet.create({ 28 | container: { 29 | marginTop: 20, 30 | flex: 1, 31 | backgroundColor: '#F5FCFF', 32 | }, 33 | }); 34 | 35 | AppRegistry.registerComponent('ListViewDemo', () => ListViewDemo); 36 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo.xcodeproj/project.pbxproj: -------------------------------------------------------------------------------- 1 | // !$*UTF8*$! 2 | { 3 | archiveVersion = 1; 4 | classes = { 5 | }; 6 | objectVersion = 46; 7 | objects = { 8 | 9 | /* Begin PBXBuildFile section */ 10 | 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */; }; 11 | 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */; }; 12 | 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */; }; 13 | 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */; }; 14 | 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */; }; 15 | 00E356F31AD99517003FC87E /* ListViewDemoTests.m in Sources */ = {isa = PBXBuildFile; fileRef = 00E356F21AD99517003FC87E /* ListViewDemoTests.m */; }; 16 | 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 78C398B91ACF4ADC00677621 /* libRCTLinking.a */; }; 17 | 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */; }; 18 | 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */; }; 19 | 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB01A68108700A75B9A /* AppDelegate.m */; }; 20 | 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB11A68108700A75B9A /* LaunchScreen.xib */; }; 21 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */ = {isa = PBXBuildFile; fileRef = 13B07FB51A68108700A75B9A /* Images.xcassets */; }; 22 | 13B07FC11A68108700A75B9A /* main.m in Sources */ = {isa = PBXBuildFile; fileRef = 13B07FB71A68108700A75B9A /* main.m */; }; 23 | 140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 24 | 146834051AC3E58100842450 /* libReact.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 146834041AC3E56700842450 /* libReact.a */; }; 25 | 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */ = {isa = PBXBuildFile; fileRef = 832341B51AAA6A8300B99B32 /* libRCTText.a */; }; 26 | /* End PBXBuildFile section */ 27 | 28 | /* Begin PBXContainerItemProxy section */ 29 | 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */ = { 30 | isa = PBXContainerItemProxy; 31 | containerPortal = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; 32 | proxyType = 2; 33 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 34 | remoteInfo = RCTActionSheet; 35 | }; 36 | 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */ = { 37 | isa = PBXContainerItemProxy; 38 | containerPortal = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; 39 | proxyType = 2; 40 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 41 | remoteInfo = RCTGeolocation; 42 | }; 43 | 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */ = { 44 | isa = PBXContainerItemProxy; 45 | containerPortal = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; 46 | proxyType = 2; 47 | remoteGlobalIDString = 58B5115D1A9E6B3D00147676; 48 | remoteInfo = RCTImage; 49 | }; 50 | 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */ = { 51 | isa = PBXContainerItemProxy; 52 | containerPortal = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; 53 | proxyType = 2; 54 | remoteGlobalIDString = 58B511DB1A9E6C8500147676; 55 | remoteInfo = RCTNetwork; 56 | }; 57 | 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */ = { 58 | isa = PBXContainerItemProxy; 59 | containerPortal = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; 60 | proxyType = 2; 61 | remoteGlobalIDString = 832C81801AAF6DEF007FA2F7; 62 | remoteInfo = RCTVibration; 63 | }; 64 | 00E356F41AD99517003FC87E /* PBXContainerItemProxy */ = { 65 | isa = PBXContainerItemProxy; 66 | containerPortal = 83CBB9F71A601CBA00E9B192 /* Project object */; 67 | proxyType = 1; 68 | remoteGlobalIDString = 13B07F861A680F5B00A75B9A; 69 | remoteInfo = ListViewDemo; 70 | }; 71 | 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */ = { 72 | isa = PBXContainerItemProxy; 73 | containerPortal = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; 74 | proxyType = 2; 75 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 76 | remoteInfo = RCTSettings; 77 | }; 78 | 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */ = { 79 | isa = PBXContainerItemProxy; 80 | containerPortal = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; 81 | proxyType = 2; 82 | remoteGlobalIDString = 3C86DF461ADF2C930047B81A; 83 | remoteInfo = RCTWebSocket; 84 | }; 85 | 146834031AC3E56700842450 /* PBXContainerItemProxy */ = { 86 | isa = PBXContainerItemProxy; 87 | containerPortal = 146833FF1AC3E56700842450 /* React.xcodeproj */; 88 | proxyType = 2; 89 | remoteGlobalIDString = 83CBBA2E1A601D0E00E9B192; 90 | remoteInfo = React; 91 | }; 92 | 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */ = { 93 | isa = PBXContainerItemProxy; 94 | containerPortal = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; 95 | proxyType = 2; 96 | remoteGlobalIDString = 134814201AA4EA6300B7C361; 97 | remoteInfo = RCTLinking; 98 | }; 99 | 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */ = { 100 | isa = PBXContainerItemProxy; 101 | containerPortal = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; 102 | proxyType = 2; 103 | remoteGlobalIDString = 58B5119B1A9E6C1200147676; 104 | remoteInfo = RCTText; 105 | }; 106 | /* End PBXContainerItemProxy section */ 107 | 108 | /* Begin PBXFileReference section */ 109 | 008F07F21AC5B25A0029DE68 /* main.jsbundle */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text; name = main.jsbundle; path = main.jsbundle; sourceTree = ""; }; 110 | 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTActionSheet.xcodeproj; path = ../node_modules/react-native/Libraries/ActionSheetIOS/RCTActionSheet.xcodeproj; sourceTree = ""; }; 111 | 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTGeolocation.xcodeproj; path = ../node_modules/react-native/Libraries/Geolocation/RCTGeolocation.xcodeproj; sourceTree = ""; }; 112 | 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTImage.xcodeproj; path = ../node_modules/react-native/Libraries/Image/RCTImage.xcodeproj; sourceTree = ""; }; 113 | 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTNetwork.xcodeproj; path = ../node_modules/react-native/Libraries/Network/RCTNetwork.xcodeproj; sourceTree = ""; }; 114 | 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTVibration.xcodeproj; path = ../node_modules/react-native/Libraries/Vibration/RCTVibration.xcodeproj; sourceTree = ""; }; 115 | 00E356EE1AD99517003FC87E /* ListViewDemoTests.xctest */ = {isa = PBXFileReference; explicitFileType = wrapper.cfbundle; includeInIndex = 0; path = ListViewDemoTests.xctest; sourceTree = BUILT_PRODUCTS_DIR; }; 116 | 00E356F11AD99517003FC87E /* Info.plist */ = {isa = PBXFileReference; lastKnownFileType = text.plist.xml; path = Info.plist; sourceTree = ""; }; 117 | 00E356F21AD99517003FC87E /* ListViewDemoTests.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = ListViewDemoTests.m; sourceTree = ""; }; 118 | 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTSettings.xcodeproj; path = ../node_modules/react-native/Libraries/Settings/RCTSettings.xcodeproj; sourceTree = ""; }; 119 | 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTWebSocket.xcodeproj; path = ../node_modules/react-native/Libraries/WebSocket/RCTWebSocket.xcodeproj; sourceTree = ""; }; 120 | 13B07F961A680F5B00A75B9A /* ListViewDemo.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = ListViewDemo.app; sourceTree = BUILT_PRODUCTS_DIR; }; 121 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; name = AppDelegate.h; path = ListViewDemo/AppDelegate.h; sourceTree = ""; }; 122 | 13B07FB01A68108700A75B9A /* AppDelegate.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = AppDelegate.m; path = ListViewDemo/AppDelegate.m; sourceTree = ""; }; 123 | 13B07FB21A68108700A75B9A /* Base */ = {isa = PBXFileReference; lastKnownFileType = file.xib; name = Base; path = Base.lproj/LaunchScreen.xib; sourceTree = ""; }; 124 | 13B07FB51A68108700A75B9A /* Images.xcassets */ = {isa = PBXFileReference; lastKnownFileType = folder.assetcatalog; name = Images.xcassets; path = ListViewDemo/Images.xcassets; sourceTree = ""; }; 125 | 13B07FB61A68108700A75B9A /* Info.plist */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = text.plist.xml; name = Info.plist; path = ListViewDemo/Info.plist; sourceTree = ""; }; 126 | 13B07FB71A68108700A75B9A /* main.m */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.objc; name = main.m; path = ListViewDemo/main.m; sourceTree = ""; }; 127 | 146833FF1AC3E56700842450 /* React.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = React.xcodeproj; path = ../node_modules/react-native/React/React.xcodeproj; sourceTree = ""; }; 128 | 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTLinking.xcodeproj; path = ../node_modules/react-native/Libraries/LinkingIOS/RCTLinking.xcodeproj; sourceTree = ""; }; 129 | 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.pb-project"; name = RCTText.xcodeproj; path = ../node_modules/react-native/Libraries/Text/RCTText.xcodeproj; sourceTree = ""; }; 130 | /* End PBXFileReference section */ 131 | 132 | /* Begin PBXFrameworksBuildPhase section */ 133 | 00E356EB1AD99517003FC87E /* Frameworks */ = { 134 | isa = PBXFrameworksBuildPhase; 135 | buildActionMask = 2147483647; 136 | files = ( 137 | 140ED2AC1D01E1AD002B40FF /* libReact.a in Frameworks */, 138 | ); 139 | runOnlyForDeploymentPostprocessing = 0; 140 | }; 141 | 13B07F8C1A680F5B00A75B9A /* Frameworks */ = { 142 | isa = PBXFrameworksBuildPhase; 143 | buildActionMask = 2147483647; 144 | files = ( 145 | 146834051AC3E58100842450 /* libReact.a in Frameworks */, 146 | 00C302E51ABCBA2D00DB3ED1 /* libRCTActionSheet.a in Frameworks */, 147 | 00C302E71ABCBA2D00DB3ED1 /* libRCTGeolocation.a in Frameworks */, 148 | 00C302E81ABCBA2D00DB3ED1 /* libRCTImage.a in Frameworks */, 149 | 133E29F31AD74F7200F7D852 /* libRCTLinking.a in Frameworks */, 150 | 00C302E91ABCBA2D00DB3ED1 /* libRCTNetwork.a in Frameworks */, 151 | 139105C61AF99C1200B5F7CC /* libRCTSettings.a in Frameworks */, 152 | 832341BD1AAA6AB300B99B32 /* libRCTText.a in Frameworks */, 153 | 00C302EA1ABCBA2D00DB3ED1 /* libRCTVibration.a in Frameworks */, 154 | 139FDEF61B0652A700C62182 /* libRCTWebSocket.a in Frameworks */, 155 | ); 156 | runOnlyForDeploymentPostprocessing = 0; 157 | }; 158 | /* End PBXFrameworksBuildPhase section */ 159 | 160 | /* Begin PBXGroup section */ 161 | 00C302A81ABCB8CE00DB3ED1 /* Products */ = { 162 | isa = PBXGroup; 163 | children = ( 164 | 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */, 165 | ); 166 | name = Products; 167 | sourceTree = ""; 168 | }; 169 | 00C302B61ABCB90400DB3ED1 /* Products */ = { 170 | isa = PBXGroup; 171 | children = ( 172 | 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */, 173 | ); 174 | name = Products; 175 | sourceTree = ""; 176 | }; 177 | 00C302BC1ABCB91800DB3ED1 /* Products */ = { 178 | isa = PBXGroup; 179 | children = ( 180 | 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */, 181 | ); 182 | name = Products; 183 | sourceTree = ""; 184 | }; 185 | 00C302D41ABCB9D200DB3ED1 /* Products */ = { 186 | isa = PBXGroup; 187 | children = ( 188 | 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */, 189 | ); 190 | name = Products; 191 | sourceTree = ""; 192 | }; 193 | 00C302E01ABCB9EE00DB3ED1 /* Products */ = { 194 | isa = PBXGroup; 195 | children = ( 196 | 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */, 197 | ); 198 | name = Products; 199 | sourceTree = ""; 200 | }; 201 | 00E356EF1AD99517003FC87E /* ListViewDemoTests */ = { 202 | isa = PBXGroup; 203 | children = ( 204 | 00E356F21AD99517003FC87E /* ListViewDemoTests.m */, 205 | 00E356F01AD99517003FC87E /* Supporting Files */, 206 | ); 207 | path = ListViewDemoTests; 208 | sourceTree = ""; 209 | }; 210 | 00E356F01AD99517003FC87E /* Supporting Files */ = { 211 | isa = PBXGroup; 212 | children = ( 213 | 00E356F11AD99517003FC87E /* Info.plist */, 214 | ); 215 | name = "Supporting Files"; 216 | sourceTree = ""; 217 | }; 218 | 139105B71AF99BAD00B5F7CC /* Products */ = { 219 | isa = PBXGroup; 220 | children = ( 221 | 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */, 222 | ); 223 | name = Products; 224 | sourceTree = ""; 225 | }; 226 | 139FDEE71B06529A00C62182 /* Products */ = { 227 | isa = PBXGroup; 228 | children = ( 229 | 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */, 230 | ); 231 | name = Products; 232 | sourceTree = ""; 233 | }; 234 | 13B07FAE1A68108700A75B9A /* ListViewDemo */ = { 235 | isa = PBXGroup; 236 | children = ( 237 | 008F07F21AC5B25A0029DE68 /* main.jsbundle */, 238 | 13B07FAF1A68108700A75B9A /* AppDelegate.h */, 239 | 13B07FB01A68108700A75B9A /* AppDelegate.m */, 240 | 13B07FB51A68108700A75B9A /* Images.xcassets */, 241 | 13B07FB61A68108700A75B9A /* Info.plist */, 242 | 13B07FB11A68108700A75B9A /* LaunchScreen.xib */, 243 | 13B07FB71A68108700A75B9A /* main.m */, 244 | ); 245 | name = ListViewDemo; 246 | sourceTree = ""; 247 | }; 248 | 146834001AC3E56700842450 /* Products */ = { 249 | isa = PBXGroup; 250 | children = ( 251 | 146834041AC3E56700842450 /* libReact.a */, 252 | ); 253 | name = Products; 254 | sourceTree = ""; 255 | }; 256 | 78C398B11ACF4ADC00677621 /* Products */ = { 257 | isa = PBXGroup; 258 | children = ( 259 | 78C398B91ACF4ADC00677621 /* libRCTLinking.a */, 260 | ); 261 | name = Products; 262 | sourceTree = ""; 263 | }; 264 | 832341AE1AAA6A7D00B99B32 /* Libraries */ = { 265 | isa = PBXGroup; 266 | children = ( 267 | 146833FF1AC3E56700842450 /* React.xcodeproj */, 268 | 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */, 269 | 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */, 270 | 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */, 271 | 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */, 272 | 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */, 273 | 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */, 274 | 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */, 275 | 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */, 276 | 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */, 277 | ); 278 | name = Libraries; 279 | sourceTree = ""; 280 | }; 281 | 832341B11AAA6A8300B99B32 /* Products */ = { 282 | isa = PBXGroup; 283 | children = ( 284 | 832341B51AAA6A8300B99B32 /* libRCTText.a */, 285 | ); 286 | name = Products; 287 | sourceTree = ""; 288 | }; 289 | 83CBB9F61A601CBA00E9B192 = { 290 | isa = PBXGroup; 291 | children = ( 292 | 13B07FAE1A68108700A75B9A /* ListViewDemo */, 293 | 832341AE1AAA6A7D00B99B32 /* Libraries */, 294 | 00E356EF1AD99517003FC87E /* ListViewDemoTests */, 295 | 83CBBA001A601CBA00E9B192 /* Products */, 296 | ); 297 | indentWidth = 2; 298 | sourceTree = ""; 299 | tabWidth = 2; 300 | }; 301 | 83CBBA001A601CBA00E9B192 /* Products */ = { 302 | isa = PBXGroup; 303 | children = ( 304 | 13B07F961A680F5B00A75B9A /* ListViewDemo.app */, 305 | 00E356EE1AD99517003FC87E /* ListViewDemoTests.xctest */, 306 | ); 307 | name = Products; 308 | sourceTree = ""; 309 | }; 310 | /* End PBXGroup section */ 311 | 312 | /* Begin PBXNativeTarget section */ 313 | 00E356ED1AD99517003FC87E /* ListViewDemoTests */ = { 314 | isa = PBXNativeTarget; 315 | buildConfigurationList = 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "ListViewDemoTests" */; 316 | buildPhases = ( 317 | 00E356EA1AD99517003FC87E /* Sources */, 318 | 00E356EB1AD99517003FC87E /* Frameworks */, 319 | 00E356EC1AD99517003FC87E /* Resources */, 320 | ); 321 | buildRules = ( 322 | ); 323 | dependencies = ( 324 | 00E356F51AD99517003FC87E /* PBXTargetDependency */, 325 | ); 326 | name = ListViewDemoTests; 327 | productName = ListViewDemoTests; 328 | productReference = 00E356EE1AD99517003FC87E /* ListViewDemoTests.xctest */; 329 | productType = "com.apple.product-type.bundle.unit-test"; 330 | }; 331 | 13B07F861A680F5B00A75B9A /* ListViewDemo */ = { 332 | isa = PBXNativeTarget; 333 | buildConfigurationList = 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ListViewDemo" */; 334 | buildPhases = ( 335 | 13B07F871A680F5B00A75B9A /* Sources */, 336 | 13B07F8C1A680F5B00A75B9A /* Frameworks */, 337 | 13B07F8E1A680F5B00A75B9A /* Resources */, 338 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */, 339 | ); 340 | buildRules = ( 341 | ); 342 | dependencies = ( 343 | ); 344 | name = ListViewDemo; 345 | productName = "Hello World"; 346 | productReference = 13B07F961A680F5B00A75B9A /* ListViewDemo.app */; 347 | productType = "com.apple.product-type.application"; 348 | }; 349 | /* End PBXNativeTarget section */ 350 | 351 | /* Begin PBXProject section */ 352 | 83CBB9F71A601CBA00E9B192 /* Project object */ = { 353 | isa = PBXProject; 354 | attributes = { 355 | LastUpgradeCheck = 0610; 356 | ORGANIZATIONNAME = Facebook; 357 | TargetAttributes = { 358 | 00E356ED1AD99517003FC87E = { 359 | CreatedOnToolsVersion = 6.2; 360 | TestTargetID = 13B07F861A680F5B00A75B9A; 361 | }; 362 | }; 363 | }; 364 | buildConfigurationList = 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ListViewDemo" */; 365 | compatibilityVersion = "Xcode 3.2"; 366 | developmentRegion = English; 367 | hasScannedForEncodings = 0; 368 | knownRegions = ( 369 | en, 370 | Base, 371 | ); 372 | mainGroup = 83CBB9F61A601CBA00E9B192; 373 | productRefGroup = 83CBBA001A601CBA00E9B192 /* Products */; 374 | projectDirPath = ""; 375 | projectReferences = ( 376 | { 377 | ProductGroup = 00C302A81ABCB8CE00DB3ED1 /* Products */; 378 | ProjectRef = 00C302A71ABCB8CE00DB3ED1 /* RCTActionSheet.xcodeproj */; 379 | }, 380 | { 381 | ProductGroup = 00C302B61ABCB90400DB3ED1 /* Products */; 382 | ProjectRef = 00C302B51ABCB90400DB3ED1 /* RCTGeolocation.xcodeproj */; 383 | }, 384 | { 385 | ProductGroup = 00C302BC1ABCB91800DB3ED1 /* Products */; 386 | ProjectRef = 00C302BB1ABCB91800DB3ED1 /* RCTImage.xcodeproj */; 387 | }, 388 | { 389 | ProductGroup = 78C398B11ACF4ADC00677621 /* Products */; 390 | ProjectRef = 78C398B01ACF4ADC00677621 /* RCTLinking.xcodeproj */; 391 | }, 392 | { 393 | ProductGroup = 00C302D41ABCB9D200DB3ED1 /* Products */; 394 | ProjectRef = 00C302D31ABCB9D200DB3ED1 /* RCTNetwork.xcodeproj */; 395 | }, 396 | { 397 | ProductGroup = 139105B71AF99BAD00B5F7CC /* Products */; 398 | ProjectRef = 139105B61AF99BAD00B5F7CC /* RCTSettings.xcodeproj */; 399 | }, 400 | { 401 | ProductGroup = 832341B11AAA6A8300B99B32 /* Products */; 402 | ProjectRef = 832341B01AAA6A8300B99B32 /* RCTText.xcodeproj */; 403 | }, 404 | { 405 | ProductGroup = 00C302E01ABCB9EE00DB3ED1 /* Products */; 406 | ProjectRef = 00C302DF1ABCB9EE00DB3ED1 /* RCTVibration.xcodeproj */; 407 | }, 408 | { 409 | ProductGroup = 139FDEE71B06529A00C62182 /* Products */; 410 | ProjectRef = 139FDEE61B06529A00C62182 /* RCTWebSocket.xcodeproj */; 411 | }, 412 | { 413 | ProductGroup = 146834001AC3E56700842450 /* Products */; 414 | ProjectRef = 146833FF1AC3E56700842450 /* React.xcodeproj */; 415 | }, 416 | ); 417 | projectRoot = ""; 418 | targets = ( 419 | 13B07F861A680F5B00A75B9A /* ListViewDemo */, 420 | 00E356ED1AD99517003FC87E /* ListViewDemoTests */, 421 | ); 422 | }; 423 | /* End PBXProject section */ 424 | 425 | /* Begin PBXReferenceProxy section */ 426 | 00C302AC1ABCB8CE00DB3ED1 /* libRCTActionSheet.a */ = { 427 | isa = PBXReferenceProxy; 428 | fileType = archive.ar; 429 | path = libRCTActionSheet.a; 430 | remoteRef = 00C302AB1ABCB8CE00DB3ED1 /* PBXContainerItemProxy */; 431 | sourceTree = BUILT_PRODUCTS_DIR; 432 | }; 433 | 00C302BA1ABCB90400DB3ED1 /* libRCTGeolocation.a */ = { 434 | isa = PBXReferenceProxy; 435 | fileType = archive.ar; 436 | path = libRCTGeolocation.a; 437 | remoteRef = 00C302B91ABCB90400DB3ED1 /* PBXContainerItemProxy */; 438 | sourceTree = BUILT_PRODUCTS_DIR; 439 | }; 440 | 00C302C01ABCB91800DB3ED1 /* libRCTImage.a */ = { 441 | isa = PBXReferenceProxy; 442 | fileType = archive.ar; 443 | path = libRCTImage.a; 444 | remoteRef = 00C302BF1ABCB91800DB3ED1 /* PBXContainerItemProxy */; 445 | sourceTree = BUILT_PRODUCTS_DIR; 446 | }; 447 | 00C302DC1ABCB9D200DB3ED1 /* libRCTNetwork.a */ = { 448 | isa = PBXReferenceProxy; 449 | fileType = archive.ar; 450 | path = libRCTNetwork.a; 451 | remoteRef = 00C302DB1ABCB9D200DB3ED1 /* PBXContainerItemProxy */; 452 | sourceTree = BUILT_PRODUCTS_DIR; 453 | }; 454 | 00C302E41ABCB9EE00DB3ED1 /* libRCTVibration.a */ = { 455 | isa = PBXReferenceProxy; 456 | fileType = archive.ar; 457 | path = libRCTVibration.a; 458 | remoteRef = 00C302E31ABCB9EE00DB3ED1 /* PBXContainerItemProxy */; 459 | sourceTree = BUILT_PRODUCTS_DIR; 460 | }; 461 | 139105C11AF99BAD00B5F7CC /* libRCTSettings.a */ = { 462 | isa = PBXReferenceProxy; 463 | fileType = archive.ar; 464 | path = libRCTSettings.a; 465 | remoteRef = 139105C01AF99BAD00B5F7CC /* PBXContainerItemProxy */; 466 | sourceTree = BUILT_PRODUCTS_DIR; 467 | }; 468 | 139FDEF41B06529B00C62182 /* libRCTWebSocket.a */ = { 469 | isa = PBXReferenceProxy; 470 | fileType = archive.ar; 471 | path = libRCTWebSocket.a; 472 | remoteRef = 139FDEF31B06529B00C62182 /* PBXContainerItemProxy */; 473 | sourceTree = BUILT_PRODUCTS_DIR; 474 | }; 475 | 146834041AC3E56700842450 /* libReact.a */ = { 476 | isa = PBXReferenceProxy; 477 | fileType = archive.ar; 478 | path = libReact.a; 479 | remoteRef = 146834031AC3E56700842450 /* PBXContainerItemProxy */; 480 | sourceTree = BUILT_PRODUCTS_DIR; 481 | }; 482 | 78C398B91ACF4ADC00677621 /* libRCTLinking.a */ = { 483 | isa = PBXReferenceProxy; 484 | fileType = archive.ar; 485 | path = libRCTLinking.a; 486 | remoteRef = 78C398B81ACF4ADC00677621 /* PBXContainerItemProxy */; 487 | sourceTree = BUILT_PRODUCTS_DIR; 488 | }; 489 | 832341B51AAA6A8300B99B32 /* libRCTText.a */ = { 490 | isa = PBXReferenceProxy; 491 | fileType = archive.ar; 492 | path = libRCTText.a; 493 | remoteRef = 832341B41AAA6A8300B99B32 /* PBXContainerItemProxy */; 494 | sourceTree = BUILT_PRODUCTS_DIR; 495 | }; 496 | /* End PBXReferenceProxy section */ 497 | 498 | /* Begin PBXResourcesBuildPhase section */ 499 | 00E356EC1AD99517003FC87E /* Resources */ = { 500 | isa = PBXResourcesBuildPhase; 501 | buildActionMask = 2147483647; 502 | files = ( 503 | ); 504 | runOnlyForDeploymentPostprocessing = 0; 505 | }; 506 | 13B07F8E1A680F5B00A75B9A /* Resources */ = { 507 | isa = PBXResourcesBuildPhase; 508 | buildActionMask = 2147483647; 509 | files = ( 510 | 13B07FBF1A68108700A75B9A /* Images.xcassets in Resources */, 511 | 13B07FBD1A68108700A75B9A /* LaunchScreen.xib in Resources */, 512 | ); 513 | runOnlyForDeploymentPostprocessing = 0; 514 | }; 515 | /* End PBXResourcesBuildPhase section */ 516 | 517 | /* Begin PBXShellScriptBuildPhase section */ 518 | 00DD1BFF1BD5951E006B06BC /* Bundle React Native code and images */ = { 519 | isa = PBXShellScriptBuildPhase; 520 | buildActionMask = 2147483647; 521 | files = ( 522 | ); 523 | inputPaths = ( 524 | ); 525 | name = "Bundle React Native code and images"; 526 | outputPaths = ( 527 | ); 528 | runOnlyForDeploymentPostprocessing = 0; 529 | shellPath = /bin/sh; 530 | shellScript = "export NODE_BINARY=node\n../node_modules/react-native/packager/react-native-xcode.sh"; 531 | showEnvVarsInLog = 1; 532 | }; 533 | /* End PBXShellScriptBuildPhase section */ 534 | 535 | /* Begin PBXSourcesBuildPhase section */ 536 | 00E356EA1AD99517003FC87E /* Sources */ = { 537 | isa = PBXSourcesBuildPhase; 538 | buildActionMask = 2147483647; 539 | files = ( 540 | 00E356F31AD99517003FC87E /* ListViewDemoTests.m in Sources */, 541 | ); 542 | runOnlyForDeploymentPostprocessing = 0; 543 | }; 544 | 13B07F871A680F5B00A75B9A /* Sources */ = { 545 | isa = PBXSourcesBuildPhase; 546 | buildActionMask = 2147483647; 547 | files = ( 548 | 13B07FBC1A68108700A75B9A /* AppDelegate.m in Sources */, 549 | 13B07FC11A68108700A75B9A /* main.m in Sources */, 550 | ); 551 | runOnlyForDeploymentPostprocessing = 0; 552 | }; 553 | /* End PBXSourcesBuildPhase section */ 554 | 555 | /* Begin PBXTargetDependency section */ 556 | 00E356F51AD99517003FC87E /* PBXTargetDependency */ = { 557 | isa = PBXTargetDependency; 558 | target = 13B07F861A680F5B00A75B9A /* ListViewDemo */; 559 | targetProxy = 00E356F41AD99517003FC87E /* PBXContainerItemProxy */; 560 | }; 561 | /* End PBXTargetDependency section */ 562 | 563 | /* Begin PBXVariantGroup section */ 564 | 13B07FB11A68108700A75B9A /* LaunchScreen.xib */ = { 565 | isa = PBXVariantGroup; 566 | children = ( 567 | 13B07FB21A68108700A75B9A /* Base */, 568 | ); 569 | name = LaunchScreen.xib; 570 | path = ListViewDemo; 571 | sourceTree = ""; 572 | }; 573 | /* End PBXVariantGroup section */ 574 | 575 | /* Begin XCBuildConfiguration section */ 576 | 00E356F61AD99517003FC87E /* Debug */ = { 577 | isa = XCBuildConfiguration; 578 | buildSettings = { 579 | BUNDLE_LOADER = "$(TEST_HOST)"; 580 | GCC_PREPROCESSOR_DEFINITIONS = ( 581 | "DEBUG=1", 582 | "$(inherited)", 583 | ); 584 | INFOPLIST_FILE = ListViewDemoTests/Info.plist; 585 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 586 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 587 | PRODUCT_NAME = "$(TARGET_NAME)"; 588 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ListViewDemo.app/ListViewDemo"; 589 | }; 590 | name = Debug; 591 | }; 592 | 00E356F71AD99517003FC87E /* Release */ = { 593 | isa = XCBuildConfiguration; 594 | buildSettings = { 595 | BUNDLE_LOADER = "$(TEST_HOST)"; 596 | COPY_PHASE_STRIP = NO; 597 | INFOPLIST_FILE = ListViewDemoTests/Info.plist; 598 | IPHONEOS_DEPLOYMENT_TARGET = 8.2; 599 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks @loader_path/Frameworks"; 600 | PRODUCT_NAME = "$(TARGET_NAME)"; 601 | TEST_HOST = "$(BUILT_PRODUCTS_DIR)/ListViewDemo.app/ListViewDemo"; 602 | }; 603 | name = Release; 604 | }; 605 | 13B07F941A680F5B00A75B9A /* Debug */ = { 606 | isa = XCBuildConfiguration; 607 | buildSettings = { 608 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 609 | DEAD_CODE_STRIPPING = NO; 610 | HEADER_SEARCH_PATHS = ( 611 | "$(inherited)", 612 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 613 | "$(SRCROOT)/../node_modules/react-native/React/**", 614 | ); 615 | INFOPLIST_FILE = "ListViewDemo/Info.plist"; 616 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 617 | OTHER_LDFLAGS = ( 618 | "$(inherited)", 619 | "-ObjC", 620 | "-lc++", 621 | ); 622 | PRODUCT_NAME = ListViewDemo; 623 | }; 624 | name = Debug; 625 | }; 626 | 13B07F951A680F5B00A75B9A /* Release */ = { 627 | isa = XCBuildConfiguration; 628 | buildSettings = { 629 | ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; 630 | HEADER_SEARCH_PATHS = ( 631 | "$(inherited)", 632 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 633 | "$(SRCROOT)/../node_modules/react-native/React/**", 634 | ); 635 | INFOPLIST_FILE = "ListViewDemo/Info.plist"; 636 | LD_RUNPATH_SEARCH_PATHS = "$(inherited) @executable_path/Frameworks"; 637 | OTHER_LDFLAGS = ( 638 | "$(inherited)", 639 | "-ObjC", 640 | "-lc++", 641 | ); 642 | PRODUCT_NAME = ListViewDemo; 643 | }; 644 | name = Release; 645 | }; 646 | 83CBBA201A601CBA00E9B192 /* Debug */ = { 647 | isa = XCBuildConfiguration; 648 | buildSettings = { 649 | ALWAYS_SEARCH_USER_PATHS = NO; 650 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 651 | CLANG_CXX_LIBRARY = "libc++"; 652 | CLANG_ENABLE_MODULES = YES; 653 | CLANG_ENABLE_OBJC_ARC = YES; 654 | CLANG_WARN_BOOL_CONVERSION = YES; 655 | CLANG_WARN_CONSTANT_CONVERSION = YES; 656 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 657 | CLANG_WARN_EMPTY_BODY = YES; 658 | CLANG_WARN_ENUM_CONVERSION = YES; 659 | CLANG_WARN_INT_CONVERSION = YES; 660 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 661 | CLANG_WARN_UNREACHABLE_CODE = YES; 662 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 663 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 664 | COPY_PHASE_STRIP = NO; 665 | ENABLE_STRICT_OBJC_MSGSEND = YES; 666 | GCC_C_LANGUAGE_STANDARD = gnu99; 667 | GCC_DYNAMIC_NO_PIC = NO; 668 | GCC_OPTIMIZATION_LEVEL = 0; 669 | GCC_PREPROCESSOR_DEFINITIONS = ( 670 | "DEBUG=1", 671 | "$(inherited)", 672 | ); 673 | GCC_SYMBOLS_PRIVATE_EXTERN = NO; 674 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 675 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 676 | GCC_WARN_UNDECLARED_SELECTOR = YES; 677 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 678 | GCC_WARN_UNUSED_FUNCTION = YES; 679 | GCC_WARN_UNUSED_VARIABLE = YES; 680 | HEADER_SEARCH_PATHS = ( 681 | "$(inherited)", 682 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 683 | "$(SRCROOT)/../node_modules/react-native/React/**", 684 | ); 685 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 686 | MTL_ENABLE_DEBUG_INFO = YES; 687 | ONLY_ACTIVE_ARCH = YES; 688 | SDKROOT = iphoneos; 689 | }; 690 | name = Debug; 691 | }; 692 | 83CBBA211A601CBA00E9B192 /* Release */ = { 693 | isa = XCBuildConfiguration; 694 | buildSettings = { 695 | ALWAYS_SEARCH_USER_PATHS = NO; 696 | CLANG_CXX_LANGUAGE_STANDARD = "gnu++0x"; 697 | CLANG_CXX_LIBRARY = "libc++"; 698 | CLANG_ENABLE_MODULES = YES; 699 | CLANG_ENABLE_OBJC_ARC = YES; 700 | CLANG_WARN_BOOL_CONVERSION = YES; 701 | CLANG_WARN_CONSTANT_CONVERSION = YES; 702 | CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; 703 | CLANG_WARN_EMPTY_BODY = YES; 704 | CLANG_WARN_ENUM_CONVERSION = YES; 705 | CLANG_WARN_INT_CONVERSION = YES; 706 | CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; 707 | CLANG_WARN_UNREACHABLE_CODE = YES; 708 | CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; 709 | "CODE_SIGN_IDENTITY[sdk=iphoneos*]" = "iPhone Developer"; 710 | COPY_PHASE_STRIP = YES; 711 | ENABLE_NS_ASSERTIONS = NO; 712 | ENABLE_STRICT_OBJC_MSGSEND = YES; 713 | GCC_C_LANGUAGE_STANDARD = gnu99; 714 | GCC_WARN_64_TO_32_BIT_CONVERSION = YES; 715 | GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; 716 | GCC_WARN_UNDECLARED_SELECTOR = YES; 717 | GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; 718 | GCC_WARN_UNUSED_FUNCTION = YES; 719 | GCC_WARN_UNUSED_VARIABLE = YES; 720 | HEADER_SEARCH_PATHS = ( 721 | "$(inherited)", 722 | /Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include, 723 | "$(SRCROOT)/../node_modules/react-native/React/**", 724 | ); 725 | IPHONEOS_DEPLOYMENT_TARGET = 7.0; 726 | MTL_ENABLE_DEBUG_INFO = NO; 727 | SDKROOT = iphoneos; 728 | VALIDATE_PRODUCT = YES; 729 | }; 730 | name = Release; 731 | }; 732 | /* End XCBuildConfiguration section */ 733 | 734 | /* Begin XCConfigurationList section */ 735 | 00E357021AD99517003FC87E /* Build configuration list for PBXNativeTarget "ListViewDemoTests" */ = { 736 | isa = XCConfigurationList; 737 | buildConfigurations = ( 738 | 00E356F61AD99517003FC87E /* Debug */, 739 | 00E356F71AD99517003FC87E /* Release */, 740 | ); 741 | defaultConfigurationIsVisible = 0; 742 | defaultConfigurationName = Release; 743 | }; 744 | 13B07F931A680F5B00A75B9A /* Build configuration list for PBXNativeTarget "ListViewDemo" */ = { 745 | isa = XCConfigurationList; 746 | buildConfigurations = ( 747 | 13B07F941A680F5B00A75B9A /* Debug */, 748 | 13B07F951A680F5B00A75B9A /* Release */, 749 | ); 750 | defaultConfigurationIsVisible = 0; 751 | defaultConfigurationName = Release; 752 | }; 753 | 83CBB9FA1A601CBA00E9B192 /* Build configuration list for PBXProject "ListViewDemo" */ = { 754 | isa = XCConfigurationList; 755 | buildConfigurations = ( 756 | 83CBBA201A601CBA00E9B192 /* Debug */, 757 | 83CBBA211A601CBA00E9B192 /* Release */, 758 | ); 759 | defaultConfigurationIsVisible = 0; 760 | defaultConfigurationName = Release; 761 | }; 762 | /* End XCConfigurationList section */ 763 | }; 764 | rootObject = 83CBB9F71A601CBA00E9B192 /* Project object */; 765 | } 766 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo.xcodeproj/xcshareddata/xcschemes/ListViewDemo.xcscheme: -------------------------------------------------------------------------------- 1 | 2 | 5 | 8 | 9 | 15 | 21 | 22 | 23 | 29 | 35 | 36 | 37 | 38 | 39 | 44 | 45 | 47 | 53 | 54 | 55 | 56 | 57 | 63 | 64 | 65 | 66 | 75 | 77 | 83 | 84 | 85 | 86 | 87 | 88 | 94 | 96 | 102 | 103 | 104 | 105 | 107 | 108 | 111 | 112 | 113 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo/AppDelegate.h: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | @interface AppDelegate : UIResponder 13 | 14 | @property (nonatomic, strong) UIWindow *window; 15 | 16 | @end 17 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo/AppDelegate.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import "AppDelegate.h" 11 | 12 | #import "RCTBundleURLProvider.h" 13 | #import "RCTRootView.h" 14 | 15 | @implementation AppDelegate 16 | 17 | - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions 18 | { 19 | NSURL *jsCodeLocation; 20 | 21 | jsCodeLocation = [[RCTBundleURLProvider sharedSettings] jsBundleURLForBundleRoot:@"index.ios" fallbackResource:nil]; 22 | 23 | RCTRootView *rootView = [[RCTRootView alloc] initWithBundleURL:jsCodeLocation 24 | moduleName:@"ListViewDemo" 25 | initialProperties:nil 26 | launchOptions:launchOptions]; 27 | rootView.backgroundColor = [[UIColor alloc] initWithRed:1.0f green:1.0f blue:1.0f alpha:1]; 28 | 29 | self.window = [[UIWindow alloc] initWithFrame:[UIScreen mainScreen].bounds]; 30 | UIViewController *rootViewController = [UIViewController new]; 31 | rootViewController.view = rootView; 32 | self.window.rootViewController = rootViewController; 33 | [self.window makeKeyAndVisible]; 34 | return YES; 35 | } 36 | 37 | @end 38 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo/Base.lproj/LaunchScreen.xib: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 21 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo/Images.xcassets/AppIcon.appiconset/Contents.json: -------------------------------------------------------------------------------- 1 | { 2 | "images" : [ 3 | { 4 | "idiom" : "iphone", 5 | "size" : "29x29", 6 | "scale" : "2x" 7 | }, 8 | { 9 | "idiom" : "iphone", 10 | "size" : "29x29", 11 | "scale" : "3x" 12 | }, 13 | { 14 | "idiom" : "iphone", 15 | "size" : "40x40", 16 | "scale" : "2x" 17 | }, 18 | { 19 | "idiom" : "iphone", 20 | "size" : "40x40", 21 | "scale" : "3x" 22 | }, 23 | { 24 | "idiom" : "iphone", 25 | "size" : "60x60", 26 | "scale" : "2x" 27 | }, 28 | { 29 | "idiom" : "iphone", 30 | "size" : "60x60", 31 | "scale" : "3x" 32 | } 33 | ], 34 | "info" : { 35 | "version" : 1, 36 | "author" : "xcode" 37 | } 38 | } -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | APPL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | LSRequiresIPhoneOS 24 | 25 | UILaunchStoryboardName 26 | LaunchScreen 27 | UIRequiredDeviceCapabilities 28 | 29 | armv7 30 | 31 | UISupportedInterfaceOrientations 32 | 33 | UIInterfaceOrientationPortrait 34 | UIInterfaceOrientationLandscapeLeft 35 | UIInterfaceOrientationLandscapeRight 36 | 37 | UIViewControllerBasedStatusBarAppearance 38 | 39 | NSLocationWhenInUseUsageDescription 40 | 41 | NSAppTransportSecurity 42 | 43 | 44 | NSExceptionDomains 45 | 46 | localhost 47 | 48 | NSTemporaryExceptionAllowsInsecureHTTPLoads 49 | 50 | 51 | 52 | 53 | 54 | 55 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemo/main.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | 12 | #import "AppDelegate.h" 13 | 14 | int main(int argc, char * argv[]) { 15 | @autoreleasepool { 16 | return UIApplicationMain(argc, argv, nil, NSStringFromClass([AppDelegate class])); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemoTests/Info.plist: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | CFBundleDevelopmentRegion 6 | en 7 | CFBundleExecutable 8 | $(EXECUTABLE_NAME) 9 | CFBundleIdentifier 10 | org.reactjs.native.example.$(PRODUCT_NAME:rfc1034identifier) 11 | CFBundleInfoDictionaryVersion 12 | 6.0 13 | CFBundleName 14 | $(PRODUCT_NAME) 15 | CFBundlePackageType 16 | BNDL 17 | CFBundleShortVersionString 18 | 1.0 19 | CFBundleSignature 20 | ???? 21 | CFBundleVersion 22 | 1 23 | 24 | 25 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/ios/ListViewDemoTests/ListViewDemoTests.m: -------------------------------------------------------------------------------- 1 | /** 2 | * Copyright (c) 2015-present, Facebook, Inc. 3 | * All rights reserved. 4 | * 5 | * This source code is licensed under the BSD-style license found in the 6 | * LICENSE file in the root directory of this source tree. An additional grant 7 | * of patent rights can be found in the PATENTS file in the same directory. 8 | */ 9 | 10 | #import 11 | #import 12 | 13 | #import "RCTLog.h" 14 | #import "RCTRootView.h" 15 | 16 | #define TIMEOUT_SECONDS 600 17 | #define TEXT_TO_LOOK_FOR @"Welcome to React Native!" 18 | 19 | @interface ListViewDemoTests : XCTestCase 20 | 21 | @end 22 | 23 | @implementation ListViewDemoTests 24 | 25 | - (BOOL)findSubviewInView:(UIView *)view matching:(BOOL(^)(UIView *view))test 26 | { 27 | if (test(view)) { 28 | return YES; 29 | } 30 | for (UIView *subview in [view subviews]) { 31 | if ([self findSubviewInView:subview matching:test]) { 32 | return YES; 33 | } 34 | } 35 | return NO; 36 | } 37 | 38 | - (void)testRendersWelcomeScreen 39 | { 40 | UIViewController *vc = [[[[UIApplication sharedApplication] delegate] window] rootViewController]; 41 | NSDate *date = [NSDate dateWithTimeIntervalSinceNow:TIMEOUT_SECONDS]; 42 | BOOL foundElement = NO; 43 | 44 | __block NSString *redboxError = nil; 45 | RCTSetLogFunction(^(RCTLogLevel level, RCTLogSource source, NSString *fileName, NSNumber *lineNumber, NSString *message) { 46 | if (level >= RCTLogLevelError) { 47 | redboxError = message; 48 | } 49 | }); 50 | 51 | while ([date timeIntervalSinceNow] > 0 && !foundElement && !redboxError) { 52 | [[NSRunLoop mainRunLoop] runMode:NSDefaultRunLoopMode beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 53 | [[NSRunLoop mainRunLoop] runMode:NSRunLoopCommonModes beforeDate:[NSDate dateWithTimeIntervalSinceNow:0.1]]; 54 | 55 | foundElement = [self findSubviewInView:vc.view matching:^BOOL(UIView *view) { 56 | if ([view.accessibilityLabel isEqualToString:TEXT_TO_LOOK_FOR]) { 57 | return YES; 58 | } 59 | return NO; 60 | }]; 61 | } 62 | 63 | RCTSetLogFunction(RCTDefaultLogFunction); 64 | 65 | XCTAssertNil(redboxError, @"RedBox error: %@", redboxError); 66 | XCTAssertTrue(foundElement, @"Couldn't find element with text '%@' in %d seconds", TEXT_TO_LOOK_FOR, TIMEOUT_SECONDS); 67 | } 68 | 69 | 70 | @end 71 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ListViewDemo", 3 | "version": "0.0.1", 4 | "private": true, 5 | "scripts": { 6 | "start": "node node_modules/react-native/local-cli/cli.js start" 7 | }, 8 | "dependencies": { 9 | "parse-lite": "^0.1.0", 10 | "react": "15.3.1", 11 | "react-native": "^0.32.1" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/src/ItemList.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ListView, 4 | RefreshControl, 5 | StyleSheet, 6 | Text, 7 | View, 8 | } from 'react-native'; 9 | 10 | export default class ItemList extends Component { 11 | _renderRow( 12 | rowData, 13 | sectionID, 14 | rowID, 15 | highlightRow 16 | ) { 17 | return ( 18 | 19 | 20 | {rowData.text} 21 | 22 | 23 | ); 24 | } 25 | 26 | _renderSeparator( 27 | sectionID, 28 | rowID, 29 | adjacentRowHighlighted 30 | ) { 31 | return ( 32 | 36 | ); 37 | } 38 | 39 | render() { 40 | if (!this.props.dataSource) { 41 | return ( 42 | 43 | Loading... 44 | 45 | ); 46 | } 47 | return ( 48 | 58 | } 59 | /> 60 | ); 61 | } 62 | } 63 | 64 | const styles = StyleSheet.create({ 65 | list: { 66 | flex: 1, 67 | }, 68 | container: { 69 | flex: 1, 70 | justifyContent: 'center', 71 | alignItems: 'center', 72 | backgroundColor: '#F5FCFF', 73 | }, 74 | loading: { 75 | fontSize: 20, 76 | textAlign: 'center', 77 | margin: 10, 78 | }, 79 | row: { 80 | flex: 1, 81 | flexDirection: 'row', 82 | alignItems: 'center', 83 | padding: 10, 84 | }, 85 | text: { 86 | fontSize: 20, 87 | }, 88 | }); 89 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/src/ItemManager.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | Modal, 4 | StyleSheet, 5 | Text, 6 | TextInput, 7 | TouchableHighlight, 8 | View, 9 | } from 'react-native'; 10 | import ItemList from './ItemList'; 11 | 12 | const TopBarButton = ({children, ...other}) => ( 13 | 14 | 15 | {children} 16 | 17 | 18 | ); 19 | 20 | export default class ItemManager extends Component { 21 | constructor() { 22 | super(); 23 | 24 | this.state = { 25 | showCreator: false, 26 | newItemText: '', 27 | }; 28 | this.addNewItem = this.addNewItem.bind(this); 29 | } 30 | 31 | addNewItem() { 32 | this.props.addItem(this.state.newItemText); 33 | this.setState({ 34 | showCreator: false, 35 | newItemText: '', 36 | }); 37 | } 38 | 39 | render() { 40 | return ( 41 | 42 | 43 | 44 | List View Demo 45 | this.setState({showCreator: true})}> 47 | {'+'} 48 | 49 | 50 | 51 | 55 | 56 | 57 | 58 | Add Item 59 | this.setState({showCreator: false})}> 61 | {'x'} 62 | 63 | 64 | 65 | this.setState({newItemText: text})} 69 | placeholder='Enter some text for your new item' 70 | /> 71 | 72 | 73 | 76 | Add Item 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | } 84 | } 85 | 86 | const styles = StyleSheet.create({ 87 | topbar: { 88 | padding: 10, 89 | flexDirection: 'row', 90 | backgroundColor: '#2975CB', 91 | justifyContent: 'space-between', 92 | alignItems: 'center', 93 | }, 94 | title: { 95 | color: '#FFFFFF', 96 | fontSize: 30, 97 | }, 98 | addItem: { 99 | width: 50, 100 | height: 50, 101 | borderColor: '#FFFFFF', 102 | borderWidth: 2, 103 | borderStyle: 'solid', 104 | alignItems: 'center', 105 | }, 106 | inputWrapper: { 107 | borderBottomWidth: 1, 108 | borderTopWidth: 1, 109 | borderStyle: 'solid', 110 | borderColor: '#CCCCCC', 111 | marginTop: 20, 112 | marginBottom: 20, 113 | }, 114 | submitButton: { 115 | width: 150, 116 | height: 50, 117 | alignItems: 'center', 118 | justifyContent: 'center', 119 | backgroundColor: '#2975CB', 120 | }, 121 | }); 122 | -------------------------------------------------------------------------------- /demos/react-native/ListViewDemo/src/RemoteData.js: -------------------------------------------------------------------------------- 1 | import React, { Component } from 'react'; 2 | import { 3 | ListView 4 | } from 'react-native'; 5 | import {App, Query, Save} from 'parse-lite'; 6 | 7 | import ItemManager from './ItemManager'; 8 | 9 | const app = new App({ 10 | host: '', // The path to your Parse Server 11 | applicationId: '', // Your Application ID 12 | }); 13 | 14 | export default class RemoteData extends Component { 15 | constructor() { 16 | super(); 17 | 18 | this.state = { 19 | rawData: [], 20 | dataSource: undefined, 21 | refreshing: false, 22 | }; 23 | this.refresh = this.refresh.bind(this); 24 | this.addItem = this.addItem.bind(this); 25 | } 26 | 27 | componentWillMount() { 28 | this.refresh(); 29 | } 30 | 31 | refresh() { 32 | this.setState({refreshing: true}); 33 | const ds = new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 34 | const q = Query.find(app, 'ListItem', Query.emptyQuery()).then((items) => { 35 | this.setState({ 36 | rawData: items, 37 | dataSource: ds.cloneWithRows(items), 38 | refreshing: false, 39 | }); 40 | }); 41 | } 42 | 43 | addItem(value) { 44 | const ds = this.state.dataSource || 45 | new ListView.DataSource({rowHasChanged: (r1, r2) => r1 !== r2}); 46 | const obj = {text: value}; 47 | const rawData = this.state.rawData.concat(obj); 48 | this.setState({ 49 | rawData: rawData, 50 | dataSource: ds.cloneWithRows(rawData), 51 | }); 52 | Save(app, 'ListItem', obj).then((o) => { 53 | const savedData = rawData.map((d) => d === obj ? o : d); 54 | this.setState({ 55 | rawData: savedData, 56 | dataSource: ds.cloneWithRows(savedData), 57 | }); 58 | }); 59 | } 60 | 61 | render() { 62 | return ( 63 | 69 | ); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/README.md: -------------------------------------------------------------------------------- 1 | # AnyBudget: A Parse Lite + React + Redux Demo 2 | 3 | This demo is a bit more complex than the Todo demo, and includes concepts like 4 | handling user login or adding ACLs to objects. It also demonstrates how multiple 5 | components automatically respond when data is modified, and how multiple 6 | components with the same queries will all wait on a single request. 7 | 8 | It's still pretty bare-bones in its design, but you could expand on it in 9 | a number of ways, including using `localStorage` or `indexedDB` to save the 10 | current user between loads. 11 | 12 | ### Try it yourself 13 | 14 | You can set this app to point to your Parse Server, and run it yourself. 15 | 16 | 1. Run `npm install` from this directory to install the development dependencies. 17 | 18 | 2. Put your Parse Server info into `./redux/Actions.js`. At the top, you'll find a line that initializes a new `App` object. Within its options parameter, set `host` to be the path to your Parse Server, and set `applicationId` to be the id of your app. 19 | ```js 20 | // INITIALIZE HERE 21 | const app = new App({ 22 | host: 'my.parse.server/path', 23 | applicationId: 'abc123', 24 | }); 25 | ``` 26 | 27 | 3. Run `npm run build` to compile your app. You'll need to run this whenever you make changes. 28 | 29 | 4. Open `index.html` in this directory. Your app should load up and point to your Parse Server. Any changes you make should be persisted when you refresh the app. 30 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/App.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import LoginWrapper from './LoginWrapper.react'; 3 | import * as Actions from '../redux/Actions'; 4 | 5 | import {connect} from 'react-redux'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | user: state.user, 10 | }; 11 | } 12 | 13 | function mapDispatchToProps(dispatch) { 14 | return { 15 | logIn: (username, password) => dispatch(Actions.logIn(username, password)), 16 | logOut: () => dispatch(Actions.logOut()), 17 | signUp: (username, password) => dispatch(Actions.signUp(username, password)), 18 | }; 19 | } 20 | 21 | export default connect( 22 | mapStateToProps, 23 | mapDispatchToProps 24 | )(LoginWrapper); 25 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/AppWrapper.react.js: -------------------------------------------------------------------------------- 1 | import * as Actions from '../redux/Actions'; 2 | import Expenses from './Expenses.react'; 3 | import Overview from './Overview.react'; 4 | import React from 'react'; 5 | import Sidebar from './Sidebar.react'; 6 | 7 | import {connect} from 'react-redux'; 8 | 9 | class AppWrapper extends React.Component { 10 | constructor(props) { 11 | super(props); 12 | 13 | this.state = {currentTab: 0}; 14 | 15 | props.fetchExpenses(); 16 | } 17 | 18 | contentsForTab(tab) { 19 | switch (tab) { 20 | case 0: 21 | return ; 22 | case 1: 23 | return ; 24 | } 25 | } 26 | 27 | render() { 28 | return ( 29 |
30 | 42 | 43 |
44 | {this.contentsForTab(this.state.currentTab)} 45 |
46 |
47 | ); 48 | } 49 | } 50 | 51 | function mapStateToProps(state) { 52 | return {}; 53 | } 54 | 55 | function mapDispatchToProps(dispatch) { 56 | return { 57 | fetchExpenses: () => dispatch(Actions.fetchExpenses()), 58 | }; 59 | } 60 | 61 | export default connect( 62 | mapStateToProps, 63 | mapDispatchToProps 64 | )(AppWrapper); 65 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/BarChart.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class BarChart extends React.Component { 4 | constructor() { 5 | super(); 6 | 7 | this.state = {width: 0}; 8 | } 9 | 10 | componentWillReceiveProps(nextProps) { 11 | let fillPercent = nextProps.value / nextProps.max * 100; 12 | if (!isFinite(fillPercent)) { 13 | fillPercent = 100; 14 | } 15 | setTimeout(() => { 16 | this.setState({width: fillPercent}); 17 | }, 0); 18 | } 19 | 20 | componentDidMount() { 21 | let fillPercent = this.props.value / this.props.max * 100; 22 | if (!isFinite(fillPercent)) { 23 | fillPercent = 100; 24 | } 25 | setTimeout(() => { 26 | this.setState({width: fillPercent}); 27 | }, 0); 28 | } 29 | 30 | render() { 31 | const fillPercent = this.props.value / this.props.max * 100; 32 | return ( 33 |
34 |
35 |
36 | ); 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/Categories.js: -------------------------------------------------------------------------------- 1 | const Categories = [ 2 | 'Select a category', 3 | 'Clothing', 4 | 'Education', 5 | 'Entertainment', 6 | 'Groceries', 7 | 'Housing', 8 | 'Medical', 9 | 'Miscellaneous', 10 | 'Savings', 11 | 'Transportation', 12 | 'Utilities', 13 | ]; 14 | 15 | export default Categories; 16 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/DonutChart.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | const DonutChart = ({segments, width, height}) => { 4 | if (!segments) { 5 | segments = []; 6 | } 7 | let total = 0; 8 | for (let i = segments.length; i--;) { 9 | total += segments[i].total; 10 | } 11 | const cx = (width / 2)|0; 12 | const cy = (height / 2)|0; 13 | const r = Math.min(cx, cy) * 0.9; 14 | 15 | let lastX = cx + r; 16 | let lastY = cy; 17 | let alpha = 0; 18 | 19 | const children = []; 20 | 21 | for (let i = 0; i < segments.length; i++) { 22 | const arc = segments[i].total / total * 2 * Math.PI; 23 | let angle = Math.min(arc, Math.PI) + alpha; 24 | let endX = r * Math.cos(angle) + cx; 25 | let endY = r * Math.sin(angle) + cy; 26 | let path = ['M', cx, cy, 'L', lastX, lastY, 'A', r, r, 0, 0, 1, endX, endY]; 27 | if (arc > Math.PI) { 28 | angle = arc + alpha; 29 | endX = r * Math.cos(angle) + cx; 30 | endY = r * Math.sin(angle) + cy; 31 | path = path.concat(['A', r, r, 0, 0, 1, endX, endY]); 32 | } 33 | path.push('Z'); 34 | children.push( 35 | 39 | ); 40 | children.push( 41 | 46 | {segments[i].id} 47 | 48 | ); 49 | 50 | lastX = endX; 51 | lastY = endY; 52 | alpha = angle; 53 | } 54 | 55 | return ( 56 | 60 | {children} 61 | 62 | 63 | ); 64 | }; 65 | 66 | export default DonutChart; 67 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/ExpenseCreator.react.js: -------------------------------------------------------------------------------- 1 | import * as Actions from '../redux/Actions'; 2 | import Categories from './Categories'; 3 | import React from 'react'; 4 | 5 | import {connect} from 'react-redux'; 6 | 7 | class ExpenseCreator extends React.Component { 8 | constructor() { 9 | super(); 10 | 11 | this.state = {fileData: ''}; 12 | 13 | this.addExpense = this.addExpense.bind(this); 14 | } 15 | 16 | render() { 17 | return ( 18 |
19 | this.name = r} 23 | placeholder='File a new expense' /> 24 | this.cost = r} 28 | placeholder='$0.00' /> 29 | 34 | 35 | Add Expense + 36 | 37 |
38 | ); 39 | } 40 | 41 | addExpense() { 42 | if ( 43 | this.name.value === '' || 44 | this.cost.value === '' || 45 | this.category.value === '' 46 | ) { 47 | return; 48 | } 49 | if (!this.cost.value.match(/^\$?\d+(\.\d*)?$/)) { 50 | return; 51 | } 52 | const costCents = Number(this.cost.value.replace('$', '')) * 100; 53 | this.props.createExpense({ 54 | name: this.name.value, 55 | category: this.category.value, 56 | costCents: costCents 57 | }).then(()=> { 58 | this.name.value = ''; 59 | this.cost.value = ''; 60 | this.category.value = ''; 61 | }); 62 | } 63 | } 64 | 65 | function mapStateToProps(state) { 66 | return {}; 67 | } 68 | 69 | function mapDispatchToProps(dispatch) { 70 | return { 71 | createExpense: (ex) => dispatch(Actions.createExpense(ex)), 72 | }; 73 | } 74 | 75 | export default connect( 76 | mapStateToProps, 77 | mapDispatchToProps 78 | )(ExpenseCreator); 79 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/Expenses.react.js: -------------------------------------------------------------------------------- 1 | import * as Actions from '../redux/Actions'; 2 | import Categories from './Categories'; 3 | import ExpenseCreator from './ExpenseCreator.react'; 4 | import React from 'react'; 5 | 6 | import {connect} from 'react-redux'; 7 | 8 | const EmptyTable = () => ( 9 |
10 |

You have no expenses this month!

11 |

(How frugal of you)

12 |
13 | ); 14 | 15 | const ExpenseTable = (props) => ( 16 |
17 | {props.expenses.map((ex) => { 18 | const costString = '$' + (ex.costCents / 100).toFixed(2); 19 | return ( 20 |
21 | {ex.name} 22 | {costString} 23 | 29 | 32 | × 33 | 34 |
35 | ); 36 | })} 37 |
38 | ); 39 | 40 | const Expenses = (props) => ( 41 |
42 | {!props.expenses ? 43 |
: 44 | (props.expenses.length ? 45 | : 46 | )} 47 | 48 |
49 | ); 50 | 51 | function mapStateToProps(state) { 52 | return { 53 | expenses: state.expenses, 54 | }; 55 | } 56 | 57 | function mapDispatchToProps(dispatch) { 58 | return { 59 | recategorize: (ex, e) => dispatch(Actions.recategorize(ex, e.target.value)), 60 | deleteExpense: (ex) => dispatch(Actions.deleteExpense(ex)), 61 | }; 62 | } 63 | 64 | export default connect( 65 | mapStateToProps, 66 | mapDispatchToProps 67 | )(Expenses); 68 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/LoginWrapper.react.js: -------------------------------------------------------------------------------- 1 | import AppWrapper from './AppWrapper.react'; 2 | import React from 'react'; 3 | 4 | export default class LoginWrapper extends React.Component { 5 | constructor() { 6 | super(); 7 | 8 | this.state = { 9 | error: null, 10 | signup: false, 11 | }; 12 | 13 | this.submit = this.submit.bind(this); 14 | this.keyDown = this.keyDown.bind(this); 15 | this.toggleSignup = this.toggleSignup.bind(this); 16 | } 17 | 18 | render() { 19 | if (this.props.user) { 20 | return ( 21 | 30 | ); 31 | } 32 | return ( 33 |
34 |

AnyBudget

35 |

Powered by Parse Lite, React & Redux

36 |
37 | {this.state.error ? 38 |
{this.state.error}
: 39 | null} 40 |
41 | 42 | this.username = r} id='username' type='text' /> 43 |
44 |
45 | 46 | this.password = r} id='password' type='password' /> 47 |
48 | 53 | 59 |
60 |
61 | ); 62 | } 63 | 64 | submit() { 65 | const username = this.username.value; 66 | const password = this.password.value; 67 | if (username.length && password.length) { 68 | if (this.state.signup) { 69 | this.props.signUp(username, password).then(() => { 70 | this.setState({error: null}); 71 | }, (err) => { 72 | console.log(err); 73 | this.setState({error: 'Invalid account information'}); 74 | }); 75 | } else { 76 | this.props.logIn(username, password).then(() => { 77 | this.setState({error: null}); 78 | }, () => { 79 | this.setState({error: 'Incorrect username or password'}); 80 | }); 81 | } 82 | } else { 83 | this.setState({error: 'Please enter all fields'}); 84 | } 85 | } 86 | 87 | keyDown(e) { 88 | if (e.keyCode === 13) { 89 | this.submit(); 90 | } 91 | } 92 | 93 | toggleSignup() { 94 | this.setState({signup: !this.state.signup}); 95 | } 96 | } 97 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/Overview.react.js: -------------------------------------------------------------------------------- 1 | import BarChart from './BarChart.react'; 2 | import React from 'react'; 3 | 4 | import {connect} from 'react-redux'; 5 | 6 | const Overview = (props) => { 7 | const totals = {}; 8 | for (let i = 0; i < props.expenses.length; i++) { 9 | const cat = props.expenses[i].category; 10 | totals[cat] = props.expenses[i].costCents + (totals[cat] || 0); 11 | } 12 | const segments = []; 13 | for (let c in totals) { 14 | segments.push({category: c, total: totals[c]/100}); 15 | } 16 | const budget = props.user.budget; 17 | 18 | let content = segments.map((s) => ( 19 |
20 | {s.category} 21 | 22 |
23 | )); 24 | 25 | if (!segments.length) { 26 | content = ( 27 |
28 |

You have no expenses this month!

29 |
30 | ); 31 | } 32 | 33 | return ( 34 |
35 | {content} 36 |
37 | ); 38 | }; 39 | 40 | function mapStateToProps(state) { 41 | return { 42 | expenses: state.expenses || [], 43 | user: state.user, 44 | }; 45 | } 46 | 47 | function mapDispatchToProps(dispatch) { 48 | return {}; 49 | } 50 | 51 | export default connect( 52 | mapStateToProps, 53 | mapDispatchToProps 54 | )(Overview); 55 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/components/Sidebar.react.js: -------------------------------------------------------------------------------- 1 | import * as Actions from '../redux/Actions'; 2 | import DonutChart from './DonutChart.react'; 3 | import React from 'react'; 4 | 5 | import {connect} from 'react-redux'; 6 | 7 | class Sidebar extends React.Component { 8 | constructor() { 9 | super(); 10 | 11 | this.state = {editingBudget: false}; 12 | 13 | this.budgetKeyDown = this.budgetKeyDown.bind(this); 14 | this.updateBudget = this.updateBudget.bind(this); 15 | this.toggleEditing = this.toggleEditing.bind(this); 16 | } 17 | 18 | render() { 19 | const budget = '$' + (this.props.user.budget || 0); 20 | const totals = {}; 21 | for (let i = 0; i < this.props.expenses.length; i++) { 22 | const cat = this.props.expenses[i].category; 23 | totals[cat] = this.props.expenses[i].costCents + (totals[cat] || 0); 24 | } 25 | const segments = []; 26 | for (let c in totals) { 27 | segments.push({id: c, total: totals[c]}); 28 | } 29 | 30 | return ( 31 |
32 |

Monthly Budget:

33 | {this.state.editingBudget ? 34 | this.budget = r} 36 | type='text' 37 | defaultValue={budget} 38 | onBlur={this.updateBudget} 39 | onKeyDown={this.budgetKeyDown} /> : 40 |
{budget}
} 41 | 42 |
43 | ); 44 | } 45 | 46 | budgetKeyDown(e) { 47 | if (e.keyCode === 13) { 48 | this.updateBudget(e); 49 | } 50 | } 51 | 52 | updateBudget(e) { 53 | const newBudget = e.target.value.replace(/\.\d*/g, '').replace(/[^\d]/g, ''); 54 | this.props.updateBudget(newBudget); 55 | this.setState({editingBudget: false}); 56 | } 57 | 58 | toggleEditing() { 59 | const shouldFocus = !this.state.editingBudget; 60 | this.setState({editingBudget: !this.state.editingBudget}, () => { 61 | if (shouldFocus) { 62 | this.budget.focus(); 63 | } 64 | }); 65 | } 66 | } 67 | 68 | function mapStateToProps(state) { 69 | return { 70 | expenses: state.expenses || [], 71 | user: state.user, 72 | }; 73 | } 74 | 75 | 76 | function mapDispatchToProps(dispatch) { 77 | return { 78 | updateBudget: (budget) => dispatch(Actions.updateBudget(budget)), 79 | }; 80 | } 81 | 82 | export default connect( 83 | mapStateToProps, 84 | mapDispatchToProps 85 | )(Sidebar); 86 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | AnyBudget - Parse Lite Demo 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/index.js: -------------------------------------------------------------------------------- 1 | import App from './components/App.react'; 2 | import {Provider} from 'react-redux'; 3 | import React from 'react'; 4 | 5 | import {createStore, applyMiddleware} from 'redux'; 6 | import thunk from 'redux-thunk'; 7 | import {render} from 'react-dom'; 8 | import budgetReducer from './redux/budgetReducer'; 9 | 10 | const store = createStore( 11 | budgetReducer, 12 | applyMiddleware(thunk), 13 | ); 14 | 15 | render( 16 | 17 | 18 | , 19 | document.getElementById('mount') 20 | ); 21 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "parse-lite": "^1.0.0", 4 | "react": "^15.3.0", 5 | "react-dom": "^15.3.0", 6 | "react-redux": "^4.4.5", 7 | "redux": "^3.5.2", 8 | "redux-thunk": "^2.1.0" 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.13.2", 12 | "babel-loader": "^6.2.5", 13 | "babel-preset-es2015": "^6.13.2", 14 | "babel-preset-react": "^6.11.1", 15 | "babel-preset-stage-1": "^6.13.0", 16 | "webpack": "^1.13.1" 17 | }, 18 | "scripts": { 19 | "build": "webpack --progress" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/redux/Actions.js: -------------------------------------------------------------------------------- 1 | import {App, Destroy, Ops, Query, Save, User} from 'parse-lite'; 2 | 3 | // INITIALIZE HERE 4 | const app = new App({ 5 | host: '', // The path to your Parse Server 6 | applicationId: '', // Your Application ID 7 | }); 8 | 9 | export function logIn(username, password) { 10 | return function(dispatch) { 11 | return User.logIn(app, {username, password}).then((auth) => { 12 | dispatch({ 13 | type: 'SET_USER_AUTH', 14 | user: auth.user, 15 | sessionToken: auth.sessionToken, 16 | }); 17 | }); 18 | }; 19 | } 20 | 21 | export function logOut() { 22 | return function(dispatch, getState) { 23 | const {sessionToken} = getState(); 24 | dispatch({type: 'SET_USER_AUTH', user: null}); 25 | return User.logOut(app, sessionToken); 26 | }; 27 | } 28 | 29 | export function signUp(username, password) { 30 | return function(dispatch) { 31 | return User.signUp(app, {username, password}).then((auth) => { 32 | dispatch({ 33 | type: 'SET_USER_AUTH', 34 | user: auth.user, 35 | sessionToken: auth.sessionToken, 36 | }); 37 | }); 38 | }; 39 | } 40 | 41 | export function fetchExpenses() { 42 | return function(dispatch, getState) { 43 | const {sessionToken} = getState(); 44 | const now = new Date(); 45 | const monthStart = new Date(now.getFullYear(), now.getMonth(), 0, 0, 0); 46 | const monthEnd = new Date(now.getFullYear(), now.getMonth() + 1, 0, 0, 0); 47 | let q = Query.ascending({}, 'createdAt'); 48 | q = Query.greaterThan(q, 'createdAt', monthStart); 49 | q = Query.lessThan(q, 'createdAt', monthEnd); 50 | return Query.find(app, 'Expense', q, {sessionToken}).then((results) => { 51 | dispatch({ 52 | type: 'FETCH_EXPENSES', 53 | expenses: results, 54 | }); 55 | }); 56 | }; 57 | } 58 | 59 | export function createExpense({name, category, costCents}) { 60 | return function(dispatch, getState) { 61 | const {user} = getState(); 62 | // Create an ACL, so that this object is only accessible to the current user 63 | const acl = {[user.objectId]: {read: true, write: true}}; 64 | return Save(app, 'Expense', { 65 | name: name, 66 | category: category, 67 | costCents: costCents, 68 | ACL: acl, 69 | }).then((ex) => { 70 | dispatch({ 71 | type: 'CREATE_EXPENSE', 72 | expense: ex, 73 | }); 74 | }); 75 | }; 76 | } 77 | 78 | export function recategorize(ex, category) { 79 | return function(dispatch, getState) { 80 | const {sessionToken} = getState(); 81 | const updated = Ops.set(ex, {category}); 82 | dispatch({type: 'UPDATE_EXPENSE', expense: updated}); 83 | return Save(app, 'Expense', updated, {sessionToken}).catch(() => { 84 | // If fails, reset to old value 85 | dispatch({type: 'UPDATE_EXPENSE', expense: ex}); 86 | }); 87 | }; 88 | } 89 | 90 | export function deleteExpense(ex) { 91 | return function(dispatch, getState) { 92 | const {sessionToken} = getState(); 93 | dispatch({type: 'DELETE_EXPENSE', expense: ex}); 94 | return Destroy(app, 'Expense', ex, {sessionToken}); 95 | }; 96 | } 97 | 98 | export function updateBudget(budget) { 99 | return function(dispatch, getState) { 100 | const {sessionToken, user} = getState(); 101 | const updated = Ops.set(user, {budget}); 102 | return Save(app, '_User', updated, {sessionToken}).then((u) => { 103 | dispatch({type: 'SET_USER_AUTH', user: u, sessionToken: sessionToken}); 104 | }); 105 | }; 106 | } 107 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/redux/budgetReducer.js: -------------------------------------------------------------------------------- 1 | const budgetReducer = (state = {}, action) => { 2 | const expenses = state.expenses || []; 3 | switch (action.type) { 4 | case 'SET_USER_AUTH': 5 | return { 6 | ...state, 7 | user: action.user, 8 | sessionToken: action.sessionToken, 9 | }; 10 | case 'FETCH_EXPENSES': 11 | return { 12 | ...state, 13 | expenses: action.expenses, 14 | }; 15 | case 'CREATE_EXPENSE': 16 | return { 17 | ...state, 18 | expenses: expenses.concat(action.expense), 19 | }; 20 | case 'UPDATE_EXPENSE': 21 | const updated = expenses.map( 22 | (e) => e.objectId === action.expense.objectId ? action.expense : e 23 | ); 24 | return { 25 | ...state, 26 | expenses: updated, 27 | }; 28 | case 'DELETE_EXPENSE': 29 | const filtered = expenses.filter( 30 | (e) => e.objectId !== action.expense.objectId 31 | ); 32 | return { 33 | ...state, 34 | expenses: filtered, 35 | }; 36 | } 37 | return state; 38 | }; 39 | 40 | const debugReducer = function(state, action) { 41 | console.log('Action:', action); 42 | console.log('Current State:', state); 43 | const next = budgetReducer(state, action); 44 | console.log('New State:', next); 45 | return next; 46 | } 47 | 48 | export default debugReducer; 49 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, Arial, sans-serif; 3 | box-sizing: border-box; 4 | background-color: #ededed; 5 | background-image: linear-gradient(#50b54a, #50b54a); 6 | background-attachment: fixed; 7 | background-size: 100% 50%; 8 | background-repeat: no-repeat; 9 | -webkit-font-smoothing: antialiased; 10 | } 11 | html, body { 12 | height: 100%; 13 | } 14 | * { 15 | box-sizing: inherit; 16 | margin: 0; 17 | padding: 0; 18 | } 19 | 20 | a { 21 | color: #59a1e8; 22 | text-decoration: none; 23 | cursor: pointer; 24 | } 25 | 26 | h1, h2, h3 { 27 | color: #475869; 28 | font-weight: 200; 29 | } 30 | 31 | h1 { 32 | font-family: 'Francois One', Arial, sans-serif; 33 | margin: 80px auto 4px auto; 34 | width: 60%; 35 | font-weight: 400; 36 | font-size: 36px; 37 | } 38 | h1 + h2 { 39 | color: #7999b9; 40 | width: 60%; 41 | font-size: 18px; 42 | margin: 0 auto 60px auto; 43 | } 44 | 45 | .centered { 46 | text-align: center; 47 | } 48 | 49 | .button { 50 | display: inline-block; 51 | padding: 0 20px; 52 | line-height: 33px; 53 | height: 36px; 54 | background: #59a1e8; 55 | border-bottom: 3px solid #475869; 56 | color: white; 57 | vertical-align: top; 58 | } 59 | 60 | input[type=text], input[type=password], select { 61 | outline: none; 62 | border: 1px solid #475869; 63 | color: #475869; 64 | background: white; 65 | font-size: 16px; 66 | border-radius: 0; 67 | padding: 0 6px; 68 | -webkit-appearance: none; 69 | } 70 | select { 71 | padding-right: 20px; 72 | background-image: url( 73 | 'data:image/svg+xml;utf8,' 74 | ); 75 | background-repeat: no-repeat; 76 | background-position: top 50% right 6px; 77 | } 78 | input[type=text]:focus, input[type=password]:focus { 79 | border-color: #59a1e8; 80 | } 81 | 82 | #mount { 83 | position: relative; 84 | border-top: 20px solid #2c7228; 85 | max-width: 1000px; 86 | width: 90%; 87 | min-height: 100%; 88 | margin: 0 auto; 89 | background: white; 90 | } 91 | 92 | .loginForm { 93 | width: 300px; 94 | border: 1px solid #c4e0f1; 95 | padding: 10px; 96 | margin: 0 auto; 97 | } 98 | .loginForm .row { 99 | padding: 10px 0; 100 | } 101 | .loginForm .row:first-child { 102 | padding-top: 0; 103 | } 104 | .loginForm .row:last-child { 105 | padding-bottom: 0; 106 | } 107 | .loginForm .row label { 108 | width: 80px; 109 | float: left; 110 | line-height: 30px; 111 | text-align: right; 112 | color: #475869; 113 | } 114 | .loginForm .row input { 115 | display: block; 116 | margin-left: 100px; 117 | padding: 4px 6px; 118 | height: 30px; 119 | width: 170px; 120 | } 121 | .loginForm .errors { 122 | color: #881a10; 123 | font-size: 14px; 124 | } 125 | 126 | .logOut { 127 | position: absolute; 128 | top: 10px; 129 | right: 10px; 130 | width: 30px; 131 | height: 30px; 132 | padding: 5px; 133 | cursor: pointer; 134 | } 135 | .logOut svg { 136 | width: 20px; 137 | height: 20px; 138 | fill: #2c7228; 139 | } 140 | .logOut:hover { 141 | background: #2c7228; 142 | } 143 | .logOut:hover svg { 144 | fill: white; 145 | } 146 | 147 | .menu { 148 | font-size: 0; 149 | text-align: center; 150 | margin: 40px 0; 151 | } 152 | .menu a { 153 | display: inline-block; 154 | border-style: solid; 155 | border-color: #c4e0f1; 156 | border-width: 0 1px; 157 | background: white; 158 | height: 48px; 159 | line-height: 48px; 160 | width: 150px; 161 | text-align: center; 162 | font-size: 24px; 163 | font-weight: 200; 164 | letter-spacing: 0.1em; 165 | font-variant: small-caps; 166 | color: #475869; 167 | cursor: pointer; 168 | } 169 | .menu a:hover { 170 | background: #c4e0f1; 171 | } 172 | .menu a:first-child { 173 | border-left-width: 2px; 174 | } 175 | .menu a:last-child { 176 | border-right-width: 2px; 177 | } 178 | .menu a.selected { 179 | background: #7999b9; 180 | border-color: #7999b9; 181 | border-bottom: 3px solid #475869; 182 | color: white; 183 | } 184 | 185 | .sidebar { 186 | float: right; 187 | width: 150px; 188 | } 189 | .mainPanel { 190 | margin-right: 150px; 191 | padding-bottom: 100px; 192 | } 193 | 194 | @media(max-width: 900px) { 195 | .sidebar { 196 | display: none; 197 | } 198 | .mainPanel { 199 | margin-right: 0; 200 | } 201 | } 202 | 203 | .sidebar .budget { 204 | width: 120px; 205 | font-size: 22px; 206 | text-align: center; 207 | cursor: pointer; 208 | } 209 | .sidebar input { 210 | text-align: right; 211 | width: 120px; 212 | height: 30px; 213 | line-height: 28px; 214 | } 215 | .sidebar h3 { 216 | font-size: 16px; 217 | width: 120px; 218 | text-align: center; 219 | margin-bottom: 10px; 220 | } 221 | .sidebar svg { 222 | margin: 20px 0 0 0; 223 | } 224 | 225 | .appContent { 226 | width: 90%; 227 | margin: 0 auto; 228 | } 229 | 230 | .barChartWrap { 231 | display: block; 232 | height: 30px; 233 | margin: 10px 0; 234 | border: 1px solid #475869; 235 | background: linear-gradient(#e0e0e0 10%, #ffffff 10%); 236 | } 237 | .barChartFill { 238 | display: block; 239 | height: 28px; 240 | width: 0; 241 | max-width: 100%; 242 | background: #50b54a; 243 | transition: width 0.3s ease-out; 244 | } 245 | 246 | .donutChart path { 247 | -webkit-transform: scale(1); 248 | transform: scale(1); 249 | transition: all 0.3s ease; 250 | } 251 | .donutChart path:nth-of-type(6n){ 252 | fill: #5E7CF4; 253 | } 254 | .donutChart path:nth-of-type(6n+1){ 255 | fill: #5DDF2A; 256 | } 257 | .donutChart path:nth-of-type(6n+2){ 258 | fill: #E25AF4; 259 | } 260 | .donutChart path:nth-of-type(6n+3){ 261 | fill: #FAE048; 262 | } 263 | .donutChart path:nth-of-type(6n+4){ 264 | fill: #55C7F4; 265 | } 266 | .donutChart path:nth-of-type(6n+5){ 267 | fill: #F48C4F; 268 | } 269 | .donutChart path:hover { 270 | -webkit-transform: scale(1.1); 271 | transform: scale(1.1); 272 | } 273 | .donutChart text { 274 | font-size: 16px; 275 | font-weight: 200; 276 | opacity: 0; 277 | transition: opacity 0.3s ease; 278 | } 279 | .donutChart path:hover + text { 280 | opacity: 1; 281 | } 282 | 283 | .expenseTable { 284 | min-height: 120px; 285 | } 286 | 287 | .expenseRow { 288 | position: relative; 289 | height: 60px; 290 | line-height: 60px; 291 | font-weight: 200; 292 | padding-right: 60px; 293 | } 294 | .expenseRow span { 295 | height: 60px; 296 | line-height: 60px; 297 | display: inline-block; 298 | vertical-align: top; 299 | } 300 | .expenseName { 301 | display: inline-block; 302 | width: 40%; 303 | height: 60px; 304 | margin-right: 10px; 305 | font-size: 18px; 306 | overflow: hidden; 307 | white-space: nowrap; 308 | text-overflow: ellipsis; 309 | } 310 | .expenseCost { 311 | display: inline-block; 312 | width: 20%; 313 | margin-right: 10px; 314 | } 315 | .expenseCategory { 316 | height: 30px; 317 | } 318 | .expenseRow .delete { 319 | position: absolute; 320 | right: 0px; 321 | top: 15px; 322 | height: 30px; 323 | width: 30px; 324 | line-height: 23px; 325 | background: #c61303; 326 | font-size: 26px; 327 | font-weight: bold; 328 | color: white; 329 | text-align: center; 330 | border-bottom: 3px solid #62112A; 331 | } 332 | 333 | .emptyTable { 334 | text-align: center; 335 | padding: 40px 0; 336 | } 337 | .emptyTable h2 { 338 | margin-bottom: 20px; 339 | } 340 | 341 | .loading { 342 | height: 152px; 343 | position: relative; 344 | } 345 | .loading:after { 346 | position: absolute; 347 | content: ''; 348 | display: block; 349 | width: 40px; 350 | height: 40px; 351 | left: 50%; 352 | margin-left: -20px; 353 | top: 40px; 354 | border-radius: 100%; 355 | background: #aaa; 356 | opacity: 1; 357 | -webkit-animation: ping 1s infinite ease-in-out; 358 | -moz-animation: ping 1s infinite ease-in-out; 359 | animation: ping 1s infinite ease-in-out; 360 | } 361 | 362 | .expenseCreator { 363 | border-top: 1px solid #e0e0e0; 364 | padding-top: 20px; 365 | } 366 | 367 | .button.upload { 368 | padding: 6px 10px; 369 | line-height: inherit; 370 | position: relative; 371 | overflow: hidden; 372 | } 373 | .upload svg { 374 | height: 20px; 375 | fill: white; 376 | } 377 | .upload input[type=file] { 378 | position: absolute; 379 | top: 0; 380 | right: 0; 381 | opacity: 0; 382 | cursor: pointer; 383 | height: 100%; 384 | min-width: 100%; 385 | } 386 | .expenseCreator input.name { 387 | width: 200px; 388 | } 389 | .expenseCreator input.cost { 390 | text-align: right; 391 | width: 80px; 392 | } 393 | .expenseCreator input[type=text], .expenseCreator select { 394 | vertical-align: top; 395 | height: 36px; 396 | margin: 0 5px; 397 | } 398 | .expenseCreator a { 399 | margin: 0 5px; 400 | } 401 | 402 | @-webkit-keyframes ping { 403 | 0% { 404 | -webkit-transform: scale(0); 405 | } 406 | 100% { 407 | -webkit-transform: scale(1); 408 | opacity: 0; 409 | } 410 | } 411 | @-moz-keyframes ping { 412 | 0% { 413 | transform: scale(0); 414 | } 415 | 100% { 416 | transform: scale(1); 417 | opacity: 0; 418 | } 419 | } 420 | @keyframes ping { 421 | 0% { 422 | transform: scale(0); 423 | } 424 | 100% { 425 | transform: scale(1); 426 | opacity: 0; 427 | } 428 | } 429 | -------------------------------------------------------------------------------- /demos/react-redux/AnyBudget/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index', 5 | output: { 6 | path: path.join(__dirname, 'build'), 7 | filename: 'app.js', 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | loaders: [ 'babel' ], 14 | exclude: /node_modules/, 15 | include: __dirname 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /demos/react-redux/README.md: -------------------------------------------------------------------------------- 1 | This directory contains demos that show how to use Parse Lite with React and 2 | Redux. Each time Parse Lite modifies an object, it generates a new immutable 3 | instance, which is great for this use case. 4 | -------------------------------------------------------------------------------- /demos/react-redux/todo/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["es2015", "stage-1", "react"] 3 | } 4 | -------------------------------------------------------------------------------- /demos/react-redux/todo/.gitignore: -------------------------------------------------------------------------------- 1 | build 2 | node_modules 3 | -------------------------------------------------------------------------------- /demos/react-redux/todo/README.md: -------------------------------------------------------------------------------- 1 | A simple Todo app with **Parse Lite**, **React**, and **Redux** 2 | 3 | ### Try it yourself 4 | 5 | You can set this app to point to your Parse Server, and run it yourself. 6 | 7 | 1. Run `npm install` from this directory to install the development dependencies. 8 | 9 | 2. Put your Parse Server info into `./redux/Actions.js`. At the top, you'll find a line that initializes a new `App` object. Within its options parameter, set `host` to be the path to your Parse Server, and set `applicationId` to be the id of your app. 10 | ```js 11 | // INITIALIZE HERE 12 | const app = new App({ 13 | host: 'my.parse.server/path', 14 | applicationId: 'abc123', 15 | }); 16 | ``` 17 | 18 | 3. Run `npm run build` to compile your app. You'll need to run this whenever you make changes. 19 | 20 | 4. Open `index.html` in this directory. Your simple Todo app should load up and point to your Parse Server. Any changes you make should be persisted when you refresh the app. 21 | -------------------------------------------------------------------------------- /demos/react-redux/todo/components/App.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoList from './TodoList.react'; 3 | import * as Actions from '../redux/Actions'; 4 | 5 | import {connect} from 'react-redux'; 6 | 7 | function mapStateToProps(state) { 8 | return { 9 | loading: state.loading, 10 | creating: state.creating, 11 | items: state.items || [], 12 | }; 13 | } 14 | 15 | function mapDispatchToProps(dispatch) { 16 | return { 17 | fetch: () => dispatch(Actions.fetch()), 18 | update: (id, text) => dispatch(Actions.update(id, text)), 19 | destroy: (id) => dispatch(Actions.destroy(id)), 20 | create: (text) => dispatch(Actions.create(text)), 21 | }; 22 | } 23 | 24 | export default connect( 25 | mapStateToProps, 26 | mapDispatchToProps 27 | )(TodoList); 28 | -------------------------------------------------------------------------------- /demos/react-redux/todo/components/PrettyDate.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | var months = [ 4 | 'January', 5 | 'February', 6 | 'March', 7 | 'April', 8 | 'May', 9 | 'June', 10 | 'July', 11 | 'August', 12 | 'September', 13 | 'October', 14 | 'November', 15 | 'December' 16 | ]; 17 | 18 | export default class PrettyDate extends React.Component { 19 | constructor(props) { 20 | super(props); 21 | 22 | this.forceUpdate = this.forceUpdate.bind(this); 23 | } 24 | 25 | componentWillMount() { 26 | this.interval = null; 27 | } 28 | 29 | componentDidMount() { 30 | const delta = (new Date() - this.props.value) / 1000; 31 | if (delta < 60 * 60) { 32 | this.setInterval(this.forceUpdate, 10000); 33 | } 34 | } 35 | 36 | componentWillUnmount() { 37 | if (this.interval) { 38 | clearInterval(this.interval); 39 | } 40 | } 41 | 42 | setInterval() { 43 | this.interval = setInterval.apply(null, arguments); 44 | } 45 | 46 | render() { 47 | const {value} = this.props; 48 | let text = ''; 49 | const delta = (new Date() - value) / 1000; 50 | 51 | if (delta < 60) { 52 | text = 'Just now'; 53 | } else if (delta < 60 * 60) { 54 | const mins = (delta / 60)|0; 55 | text = mins + (mins === 1 ? ' minute ago' : ' minutes ago'); 56 | } else if (delta < 60 * 60 * 24) { 57 | const hours = (delta / 60 / 60)|0; 58 | text = hours + (hours === 1 ? ' hour ago' : ' hours ago'); 59 | } else { 60 | text = value.getDate() + ' ' + months[value.getMonth()]; 61 | } 62 | 63 | return {text}; 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /demos/react-redux/todo/components/TodoCreator.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | 3 | export default class TodoCreator extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | 7 | this.state = { 8 | value: '', 9 | }; 10 | 11 | this.onChange = this.onChange.bind(this); 12 | this.onKeyDown = this.onKeyDown.bind(this); 13 | this.submit = this.submit.bind(this); 14 | } 15 | 16 | componentWillReceiveProps(nextProps) { 17 | if (this.props.creating && !nextProps.creating) { 18 | this.setState({ 19 | value: '', 20 | }); 21 | } 22 | } 23 | 24 | render() { 25 | return ( 26 |
27 | 33 | Add 34 |
35 | ); 36 | } 37 | 38 | onChange(e) { 39 | this.setState({ 40 | value: e.target.value, 41 | }); 42 | } 43 | 44 | onKeyDown(e) { 45 | if (e.keyCode === 13) { 46 | this.submit(); 47 | } 48 | } 49 | 50 | submit() { 51 | if (!this.props.creating) { 52 | this.props.create(this.state.value); 53 | } 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /demos/react-redux/todo/components/TodoItem.react.js: -------------------------------------------------------------------------------- 1 | import PrettyDate from './PrettyDate.react'; 2 | import React from 'react'; 3 | 4 | export default class TodoItem extends React.Component { 5 | constructor(props) { 6 | super(props); 7 | 8 | this.state = { 9 | editing: false, 10 | editText: '', 11 | }; 12 | 13 | this.onChange = this.onChange.bind(this); 14 | this.onKeyDown = this.onKeyDown.bind(this); 15 | this.startEdit = this.startEdit.bind(this); 16 | this.stopEdit = this.stopEdit.bind(this); 17 | this.deleteItem = this.deleteItem.bind(this); 18 | } 19 | 20 | render() { 21 | const {editing, editText} = this.state; 22 | if (editing) { 23 | return ( 24 |
25 | this.inputRef = i} 27 | onChange={this.onChange} 28 | onKeyDown={this.onKeyDown} 29 | value={editText} 30 | /> 31 | 32 | 33 | 34 |
35 | ); 36 | } 37 | const {item} = this.props; 38 | return ( 39 |
40 |
41 | {item.text} 42 |
43 | 44 | 45 |
46 |
47 |
48 | 49 |
50 |
51 | ); 52 | } 53 | 54 | startEdit() { 55 | this.setState({ 56 | editText: this.props.item.text, 57 | editing: true, 58 | }, () => { 59 | const node = this.inputRef; 60 | node.focus(); 61 | const len = this.state.editText.length; 62 | node.setSelectionRange(len, len); 63 | }); 64 | } 65 | 66 | onChange(e) { 67 | this.setState({ 68 | editText: e.target.value, 69 | }); 70 | } 71 | 72 | onKeyDown(e) { 73 | if (e.keyCode === 13) { 74 | this.stopEdit(); 75 | } 76 | } 77 | 78 | stopEdit() { 79 | if (this.state.editText) { 80 | this.props.update(this.props.item, this.state.editText); 81 | this.setState({ 82 | editing: false, 83 | }); 84 | } else { 85 | this.props.destroy(this.props.item.objectId); 86 | } 87 | } 88 | 89 | deleteItem() { 90 | this.props.destroy(this.props.item.objectId); 91 | } 92 | } 93 | -------------------------------------------------------------------------------- /demos/react-redux/todo/components/TodoList.react.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import TodoCreator from './TodoCreator.react'; 3 | import TodoItem from './TodoItem.react'; 4 | 5 | export default class TodoList extends React.Component { 6 | constructor(props) { 7 | super(props); 8 | 9 | this.refresh = this.refresh.bind(this); 10 | } 11 | 12 | componentWillMount() { 13 | this.props.fetch(); 14 | } 15 | 16 | refresh() { 17 | this.props.fetch(); 18 | } 19 | 20 | render() { 21 | return ( 22 |
23 | Refresh 24 | {this.props.items.map((i) => 25 | 31 | )} 32 | 33 |
34 | ); 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /demos/react-redux/todo/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Todo - Parse Lite Demo 7 | 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /demos/react-redux/todo/index.js: -------------------------------------------------------------------------------- 1 | import App from './components/App.react'; 2 | import {Provider} from 'react-redux'; 3 | import React from 'react'; 4 | 5 | import {createStore, applyMiddleware} from 'redux'; 6 | import thunk from 'redux-thunk'; 7 | import {render} from 'react-dom'; 8 | import todoReducer from './redux/todoReducer'; 9 | 10 | const store = createStore( 11 | todoReducer, 12 | applyMiddleware(thunk), 13 | ); 14 | 15 | render( 16 | 17 | 18 | , 19 | document.getElementById('mount') 20 | ); 21 | -------------------------------------------------------------------------------- /demos/react-redux/todo/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "parse-lite": "^1.0.0", 4 | "react": "^15.3.0", 5 | "react-dom": "^15.3.0", 6 | "react-redux": "^4.4.5", 7 | "redux": "^3.5.2", 8 | "redux-thunk": "^2.1.0" 9 | }, 10 | "devDependencies": { 11 | "babel-core": "^6.13.2", 12 | "babel-loader": "^6.2.5", 13 | "babel-preset-es2015": "^6.13.2", 14 | "babel-preset-react": "^6.11.1", 15 | "babel-preset-stage-1": "^6.13.0", 16 | "webpack": "^1.13.1" 17 | }, 18 | "scripts": { 19 | "build": "webpack --progress" 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /demos/react-redux/todo/redux/Actions.js: -------------------------------------------------------------------------------- 1 | import {App, Ops, Query, Save, Destroy} from 'parse-lite'; 2 | 3 | // INITIALIZE HERE 4 | const app = new App({ 5 | host: '', // The path to your Parse Server 6 | applicationId: '', // Your Application ID 7 | }); 8 | 9 | export function fetch() { 10 | return function(dispatch) { 11 | dispatch({type: 'FETCH_ITEMS'}); 12 | const q = Query.ascending({}, 'createdAt'); 13 | Query.find(app, 'TodoItem', q).then((items) => { 14 | dispatch({ 15 | type: 'FINISH_FETCH_ITEMS', 16 | items: items, 17 | }); 18 | }); 19 | }; 20 | } 21 | 22 | export function update(item, text) { 23 | return function(dispatch) { 24 | dispatch({ 25 | type: 'EDIT_ITEM', 26 | id: item.objectId, 27 | text: text, 28 | }); 29 | const updated = Ops.set(item, {text}); 30 | Save(app, 'TodoItem', updated); 31 | }; 32 | } 33 | 34 | export function destroy(id) { 35 | return function(dispatch) { 36 | dispatch({ 37 | type: 'DELETE_ITEM', 38 | id: id, 39 | }); 40 | Destroy(app, 'TodoItem', id); 41 | }; 42 | } 43 | 44 | export function create(text) { 45 | return function(dispatch) { 46 | dispatch({type: 'CREATE_ITEM'}); 47 | Save(app, 'TodoItem', {text}).then((item) => { 48 | dispatch({ 49 | type: 'FINISH_CREATE_ITEM', 50 | item: item, 51 | }); 52 | }); 53 | }; 54 | } 55 | -------------------------------------------------------------------------------- /demos/react-redux/todo/redux/todoReducer.js: -------------------------------------------------------------------------------- 1 | import {Ops, Save} from 'parse-lite'; 2 | 3 | const itemReducer = (state = {}, action) => { 4 | switch (action.type) { 5 | case 'CREATE_ITEM': 6 | return Ops.set(state, { 7 | id: action.id, 8 | text: action.text, 9 | createdAt: new Date(), 10 | }); 11 | case 'EDIT_ITEM': 12 | return Ops.set(state, {text: action.text}); 13 | } 14 | return state; 15 | }; 16 | 17 | const todoReducer = (state = {}, action) => { 18 | const items = state.items || []; 19 | switch (action.type) { 20 | case 'FETCH_ITEMS': 21 | return { 22 | ...state, 23 | loading: true, 24 | items: [], 25 | }; 26 | case 'FINISH_FETCH_ITEMS': 27 | return { 28 | ...state, 29 | loading: false, 30 | items: action.items, 31 | }; 32 | case 'CREATE_ITEM': 33 | return { 34 | ...state, 35 | creating: true, 36 | }; 37 | case 'FINISH_CREATE_ITEM': 38 | return { 39 | ...state, 40 | creating: false, 41 | items: items.concat([action.item]), 42 | }; 43 | case 'DELETE_ITEM': 44 | return { 45 | ...state, 46 | items: items.filter(item => item.objectId !== action.id), 47 | }; 48 | case 'EDIT_ITEM': 49 | return { 50 | ...state, 51 | items: items.map( 52 | item => item.objectId === action.id ? itemReducer(item, action) : item 53 | ), 54 | }; 55 | } 56 | return state; 57 | }; 58 | 59 | const debugReducer = function(state, action) { 60 | console.log('Action:', action); 61 | console.log('Current State:', state); 62 | const next = todoReducer(state, action); 63 | console.log('New State:', next); 64 | return next; 65 | } 66 | 67 | export default debugReducer; 68 | -------------------------------------------------------------------------------- /demos/react-redux/todo/styles.css: -------------------------------------------------------------------------------- 1 | body { 2 | font-family: 'Helvetica Neue', Helvetica, sans-serif; 3 | } 4 | .todoList { 5 | position: relative; 6 | width: 552px; 7 | margin: 50px auto; 8 | border: 1px solid #acc1ec; 9 | padding: 10px 15px; 10 | } 11 | .todoList.loading { 12 | padding-top: 100px; 13 | } 14 | .todoList.loading:after { 15 | position: absolute; 16 | content: ''; 17 | display: block; 18 | width: 40px; 19 | height: 40px; 20 | left: 50%; 21 | margin-left: -20px; 22 | top: 40px; 23 | border-radius: 100%; 24 | background: #aaa; 25 | opacity: 1; 26 | -webkit-animation: ping 1s infinite ease-in-out; 27 | animation: ping 1s infinite ease-in-out; 28 | } 29 | .todoList.loading .todoItem { 30 | display: none; 31 | } 32 | .todoItem { 33 | position: relative; 34 | padding: 5px 0; 35 | } 36 | .todoItem.editing { 37 | padding: 0; 38 | } 39 | .itemText { 40 | font-size: 20px; 41 | line-height: 24px; 42 | } 43 | .itemDate { 44 | font-size: 14px; 45 | color: #a9b2ba; 46 | } 47 | .todoItem input { 48 | width: 502px; 49 | height: 50px; 50 | padding: 0 4px; 51 | color: #666; 52 | } 53 | .options { 54 | position: absolute; 55 | top: 0; 56 | right: 0; 57 | overflow: hidden; 58 | white-space: nowrap; 59 | width: 0; 60 | transition: width 0.4s ease-in-out; 61 | } 62 | .todoItem:not(.editing):hover .options { 63 | width: 100px; 64 | } 65 | .save { 66 | position: absolute; 67 | top: 0; 68 | right: 0; 69 | } 70 | .todoItem a { 71 | display: inline-block; 72 | vertical-align: top; 73 | width: 32px; 74 | height: 32px; 75 | padding: 9px; 76 | background: #2c5fed; 77 | cursor: pointer; 78 | transition: background 0.2s ease; 79 | } 80 | .todoItem a:hover { 81 | background: #0028a2; 82 | } 83 | .editIcon, .deleteIcon, .submitIcon { 84 | width: 32px; 85 | height: 32px; 86 | } 87 | .editIcon:before { 88 | content: ''; 89 | display: block; 90 | width: 24px; 91 | height: 9px; 92 | background: white; 93 | -webkit-transform-origin: 50% 50%; 94 | -webkit-transform: translate(5px, 9px) rotate(-45deg); 95 | } 96 | .editIcon:after { 97 | content: ''; 98 | display: block; 99 | width: 0px; 100 | height: 0px; 101 | border-style: solid; 102 | border-width: 3px; 103 | border-color: transparent transparent white white; 104 | -webkit-transform: translate(3px, 12px); 105 | } 106 | .deleteIcon:before, .deleteIcon:after { 107 | content: ''; 108 | display: block; 109 | width: 32px; 110 | height: 10px; 111 | background: white; 112 | -webkit-transform-origin: 16px 5px; 113 | } 114 | .deleteIcon:before { 115 | -webkit-transform: translate(0, 11px) rotate(-45deg); 116 | } 117 | .deleteIcon:after { 118 | -webkit-transform: translate(0, 1px) rotate(45deg); 119 | } 120 | .submitIcon:before, .submitIcon:after { 121 | content: ''; 122 | display: block; 123 | height: 10px; 124 | background: white; 125 | -webkit-transform-origin: 50% 50%; 126 | } 127 | .submitIcon:before { 128 | width: 30px; 129 | -webkit-transform: translate(4px, 11px) rotate(-45deg); 130 | } 131 | .submitIcon:after { 132 | width: 18px; 133 | -webkit-transform: translate(-1px, 6px) rotate(45deg); 134 | } 135 | .refresh { 136 | color: #acc1ec; 137 | font-size: 14px; 138 | position: absolute; 139 | top: -24px; 140 | right: 0; 141 | cursor: pointer; 142 | } 143 | .refresh:hover { 144 | color: #6d83d4; 145 | } 146 | .todoCreator { 147 | margin-top: 10px; 148 | width: 552px; 149 | } 150 | .todoList input { 151 | box-sizing: border-box; 152 | border-style: solid; 153 | border-color: #cecece; 154 | border-width: 1px 0 1px 1px; 155 | font-size: 20px; 156 | outline: none; 157 | vertical-align: bottom; 158 | } 159 | .todoCreator input { 160 | width: 452px; 161 | padding: 6px 4px; 162 | height: 34px; 163 | } 164 | .todoCreator a { 165 | display: inline-block; 166 | background: #2c5fed; 167 | color: white; 168 | line-height: 34px; 169 | text-align: center; 170 | vertical-align: bottom; 171 | width: 100px; 172 | cursor: pointer; 173 | transition: background 0.2s ease; 174 | } 175 | .todoCreator a:hover { 176 | background: #0028a2; 177 | } 178 | 179 | @-webkit-keyframes ping { 180 | 0% { 181 | -webkit-transform: scale(0); 182 | } 183 | 100% { 184 | -webkit-transform: scale(1); 185 | opacity: 0; 186 | } 187 | } 188 | -------------------------------------------------------------------------------- /demos/react-redux/todo/webpack.config.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | module.exports = { 4 | entry: './index', 5 | output: { 6 | path: path.join(__dirname, 'build'), 7 | filename: 'app.js', 8 | }, 9 | module: { 10 | loaders: [ 11 | { 12 | test: /\.js$/, 13 | loaders: [ 'babel' ], 14 | exclude: /node_modules/, 15 | include: __dirname 16 | } 17 | ] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | 2 | exports.App = require('./lib/App').default; 3 | exports.Cloud = require('./lib/Cloud').default; 4 | exports.Destroy = require('./lib/Destroy').default; 5 | exports.Ops = require('./lib/Ops'); 6 | exports.Query = require('./lib/Query'); 7 | exports.Save = require('./lib/Save').default; 8 | exports.User = require('./lib/User'); 9 | -------------------------------------------------------------------------------- /integration/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | logs/ 3 | -------------------------------------------------------------------------------- /integration/README.md: -------------------------------------------------------------------------------- 1 | ## Integration tests 2 | 3 | These tests run against a local instance of `parse-server` 4 | -------------------------------------------------------------------------------- /integration/cloud/main.js: -------------------------------------------------------------------------------- 1 | Parse.Cloud.define('hello', function(req, res) { 2 | res.success('Hi'); 3 | }); 4 | 5 | Parse.Cloud.define('number', function(req, res) { 6 | var val = req.params.value; 7 | var obj = new Parse.Object('Num'); 8 | obj.set({ 9 | value: req.params.hasOwnProperty('value') ? req.params.value : -1, 10 | }); 11 | obj.save().then(function(o) { 12 | res.success(o); 13 | }); 14 | }); 15 | 16 | Parse.Cloud.define('problem', function(req, res) { 17 | res.error('Oops'); 18 | }); 19 | -------------------------------------------------------------------------------- /integration/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": { 3 | "babel-register": "^6.5.2", 4 | "express": "^4.13.4", 5 | "mocha": "^2.4.5", 6 | "parse-server": "2.2.17", 7 | "chai": "^3.5.0" 8 | }, 9 | "scripts": { 10 | "test": "mocha --ui tdd --compilers js:babel-register" 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /integration/server.js: -------------------------------------------------------------------------------- 1 | var express = require('express'); 2 | var ParseServer = require('parse-server').ParseServer; 3 | var app = express(); 4 | 5 | // Specify the connection string for your mongodb database 6 | // and the location to your Parse cloud code 7 | var api = new ParseServer({ 8 | databaseURI: 'mongodb://localhost:27017/integration', 9 | appId: 'integration', 10 | masterKey: 'notsosecret', 11 | serverURL: 'http://localhost:1337/parse', // Don't forget to change to https if needed 12 | cloud: __dirname + '/cloud/main.js', 13 | }); 14 | 15 | // Serve the Parse API on the /parse URL prefix 16 | app.use('/parse', api); 17 | 18 | const TestUtils = require('parse-server').TestUtils; 19 | 20 | app.get('/clear', (req, res) => { 21 | TestUtils.destroyAllDataPermanently().then(() => { 22 | res.send('{}'); 23 | }); 24 | }); 25 | 26 | app.listen(1337, () => { 27 | console.log('parse-server running on port 1337.'); 28 | }); 29 | -------------------------------------------------------------------------------- /integration/test/ArrayOperationsTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import clearApps from './clearApps'; 4 | import {expect} from 'chai'; 5 | import {App, Ops, Query, Save} from '../../index'; 6 | import {Client} from 'ibeam'; 7 | import HttpController from 'ibeam/http-node'; 8 | 9 | const app = new App({ 10 | applicationId: 'integration', 11 | https: false, 12 | host: 'localhost:1337/parse', 13 | httpController: HttpController, 14 | }); 15 | 16 | const rawClient = new Client({ 17 | https: false, 18 | host: 'localhost:1337', 19 | httpController: HttpController, 20 | }); 21 | 22 | suite('Array Operations', () => { 23 | beforeEach((done) => { 24 | clearApps(rawClient).then(() => { 25 | done(); 26 | }, (err) => { 27 | console.log('Clear App Error:', err); 28 | }); 29 | }); 30 | 31 | test('adding values', (done) => { 32 | Save(app, 'TestObject', { strings: ['foo'] }).then((o) => { 33 | expect(o.strings).to.deep.equal(['foo']); 34 | 35 | let add = Ops.add(o, 'strings', ['foo', 'bar', 'baz']); 36 | return Save(app, 'TestObject', add); 37 | }).then((o) => { 38 | expect(o.strings).to.deep.equal(['foo', 'foo', 'bar', 'baz']); 39 | done(); 40 | }).catch(e => console.log(e)); 41 | }); 42 | 43 | test('adding values on a fresh object', (done) => { 44 | Save(app, 'TestObject', {}).then((o) => { 45 | let add = Ops.add(o, 'strings', ['foo', 'bar', 'baz']); 46 | return Save(app, 'TestObject', add); 47 | }).then((o) => { 48 | expect(o.strings).to.deep.equal(['foo', 'bar', 'baz']); 49 | done(); 50 | }).catch(e => console.log(e)); 51 | }); 52 | 53 | test('combining set with add', (done) => { 54 | Save(app, 'TestObject', {}).then((o) => { 55 | let set = Ops.set(o, {strings: ['bar']}); 56 | let add = Ops.add(set, 'strings', ['baz']); 57 | expect(add.strings).to.deep.equal(['bar', 'baz']); 58 | expect(add._opSet.strings.__op).to.equal('Add'); 59 | return Save(app, 'TestObject', add); 60 | }).then((o) => { 61 | expect(o.strings).to.deep.equal(['baz']); 62 | done(); 63 | }).catch(e => console.log(e)); 64 | }); 65 | 66 | test('adding unique values', (done) => { 67 | Save(app, 'TestObject', { strings: ['foo'] }).then((o) => { 68 | expect(o.strings).to.deep.equal(['foo']); 69 | 70 | let add = Ops.addUnique(o, 'strings', ['foo', 'bar', 'baz']); 71 | return Save(app, 'TestObject', add); 72 | }).then((o) => { 73 | expect(o.strings).to.deep.equal(['foo', 'bar', 'baz']); 74 | done(); 75 | }).catch(e => console.log(e)); 76 | }); 77 | 78 | test('removing values', (done) => { 79 | Save(app, 'TestObject', { strings: ['foo', 'bar', 'baz'] }).then((o) => { 80 | expect(o.strings).to.deep.equal(['foo', 'bar', 'baz']); 81 | 82 | let add = Ops.remove(o, 'strings', ['bar', 'bat']); 83 | return Save(app, 'TestObject', add); 84 | }).then((o) => { 85 | expect(o.strings).to.deep.equal(['foo', 'baz']); 86 | done(); 87 | }).catch(e => console.log(e)); 88 | }); 89 | }); 90 | -------------------------------------------------------------------------------- /integration/test/CloudTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import clearApps from './clearApps'; 4 | import {expect} from 'chai'; 5 | import {App, Cloud} from '../../index'; 6 | import {Client} from 'ibeam'; 7 | import HttpController from 'ibeam/http-node'; 8 | 9 | const app = new App({ 10 | applicationId: 'integration', 11 | https: false, 12 | host: 'localhost:1337/parse', 13 | httpController: HttpController, 14 | }); 15 | 16 | const rawClient = new Client({ 17 | https: false, 18 | host: 'localhost:1337', 19 | httpController: HttpController, 20 | }); 21 | 22 | suite('Cloud', () => { 23 | beforeEach((done) => { 24 | clearApps(rawClient).then(() => { 25 | done(); 26 | }, (err) => { 27 | console.log('Clear App Error:', err); 28 | }); 29 | }); 30 | 31 | test('basic cloud function', (done) => { 32 | Cloud(app, 'hello').then((r) => { 33 | expect(r).to.equal('Hi'); 34 | done(); 35 | }).catch(e => console.log(e)); 36 | }); 37 | 38 | test('receiving an object', (done) => { 39 | Cloud(app, 'number').then((o) => { 40 | expect(o.objectId).to.exist; 41 | expect(o.className).to.equal('Num'); 42 | expect(o.value).to.equal(-1); 43 | done(); 44 | }).catch(e => console.log(e)); 45 | }); 46 | 47 | test('passing values', (done) => { 48 | Cloud(app, 'number', {value: 12}).then((o) => { 49 | expect(o.objectId).to.exist; 50 | expect(o.className).to.equal('Num'); 51 | expect(o.value).to.equal(12); 52 | done(); 53 | }).catch(e => console.log(e)); 54 | }); 55 | 56 | test('receiving an error', (done) => { 57 | Cloud(app, 'problem').catch((err) => { 58 | expect(err.code).to.equal(141); 59 | expect(err.error).to.equal('Oops'); 60 | done(); 61 | }).catch(e => console.log(e)); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /integration/test/DestroyTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import clearApps from './clearApps'; 4 | import {expect} from 'chai'; 5 | import {App, Destroy, Ops, Query, Save} from '../../index'; 6 | import {Client} from 'ibeam'; 7 | import HttpController from 'ibeam/http-node'; 8 | 9 | const app = new App({ 10 | applicationId: 'integration', 11 | https: false, 12 | host: 'localhost:1337/parse', 13 | httpController: HttpController, 14 | }); 15 | 16 | const rawClient = new Client({ 17 | https: false, 18 | host: 'localhost:1337', 19 | httpController: HttpController, 20 | }); 21 | 22 | suite('Destroy', () => { 23 | beforeEach((done) => { 24 | clearApps(rawClient).then(() => { 25 | done(); 26 | }, (err) => { 27 | console.log('Clear App Error:', err); 28 | }); 29 | }); 30 | 31 | test('destroying an object', (done) => { 32 | let obj; 33 | Save(app, 'TestObject', {}).then((o) => { 34 | obj = o; 35 | return Destroy(app, 'TestObject', o); 36 | }).then(() => { 37 | return Query.get(app, 'TestObject', obj); 38 | }).catch(({status, response}) => { 39 | expect(status).to.equal(404); 40 | expect(response.code).to.equal(101); 41 | done(); 42 | }).catch(err => console.log(err)); 43 | }); 44 | 45 | test('destroying an object by id', (done) => { 46 | let id; 47 | Save(app, 'TestObject', {}).then((o) => { 48 | id = o.objectId; 49 | return Destroy(app, 'TestObject', o); 50 | }).then(() => { 51 | return Query.get(app, 'TestObject', id); 52 | }).catch(({status, response}) => { 53 | expect(status).to.equal(404); 54 | expect(response.code).to.equal(101); 55 | done(); 56 | }).catch(err => console.log(err)); 57 | }); 58 | 59 | test('destroying an object that does not exist', (done) => { 60 | Destroy(app, 'TestObject', 'abc123').then(() => { 61 | throw new Error('Destroy should fail'); 62 | }, ({status, response}) => { 63 | expect(status).to.equal(404); 64 | expect(response.code).to.equal(101); 65 | done(); 66 | }).catch(err => console.log(err)); 67 | }); 68 | }); 69 | -------------------------------------------------------------------------------- /integration/test/IncrementTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import clearApps from './clearApps'; 4 | import {expect} from 'chai'; 5 | import {App, Ops, Query, Save} from '../../index'; 6 | import {Client} from 'ibeam'; 7 | import HttpController from 'ibeam/http-node'; 8 | 9 | const app = new App({ 10 | applicationId: 'integration', 11 | https: false, 12 | host: 'localhost:1337/parse', 13 | httpController: HttpController, 14 | }); 15 | 16 | const rawClient = new Client({ 17 | https: false, 18 | host: 'localhost:1337', 19 | httpController: HttpController, 20 | }); 21 | 22 | suite('Increment', () => { 23 | beforeEach((done) => { 24 | clearApps(rawClient).then(() => { 25 | done(); 26 | }, (err) => { 27 | console.log('Clear App Error:', err); 28 | }); 29 | }); 30 | 31 | test('incrementing a field', (done) => { 32 | Save(app, 'TestObject', {score: 1}).then((o) => { 33 | expect(o.score).to.equal(1); 34 | let inc = Ops.increment(o, 'score'); 35 | return Save(app, 'TestObject', inc); 36 | }).then((o) => { 37 | expect(o.score).to.equal(2); 38 | done(); 39 | }).catch((err) => console.log(err)); 40 | }); 41 | 42 | test('incrementing on a fresh object', (done) => { 43 | Save(app, 'TestObject', {}).then((o) => { 44 | let inc = Ops.increment(o, 'score'); 45 | return Save(app, 'TestObject', inc); 46 | }).then((o) => { 47 | expect(o.score).to.equal(1); 48 | done(); 49 | }).catch((err) => console.log(err)); 50 | }); 51 | 52 | test('incrementing by a value', (done) => { 53 | Save(app, 'TestObject', {score: 1}).then((o) => { 54 | let inc = Ops.increment(o, 'score', 10); 55 | return Save(app, 'TestObject', inc); 56 | }).then((o) => { 57 | expect(o.score).to.equal(11); 58 | let inc = Ops.increment(o, 'score', -5); 59 | return Save(app, 'TestObject', inc); 60 | }).then((o) => { 61 | expect(o.score).to.equal(6); 62 | done(); 63 | }).catch((err) => console.log(err)); 64 | }); 65 | 66 | test('increments are atomic', (done) => { 67 | let obj = null; 68 | Save(app, 'TestObject', {score: 1}).then((o) => { 69 | obj = o; 70 | let inc5 = Ops.increment(o, 'score', 5); 71 | return Save(app, 'TestObject', inc5); 72 | }).then((o) => { 73 | expect(o.score).to.equal(6); 74 | let inc10 = Ops.increment(obj, 'score', 10); 75 | expect(inc10.score).to.equal(11); 76 | return Save(app, 'TestObject', inc10); 77 | }).then((o) => { 78 | expect(o.score).to.equal(16); 79 | done(); 80 | }).catch((err) => console.log(err)); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /integration/test/QueryTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import clearApps from './clearApps'; 4 | import {expect} from 'chai'; 5 | import {App, Destroy, Ops, Query, Save} from '../../index'; 6 | import {Client} from 'ibeam'; 7 | import HttpController from 'ibeam/http-node'; 8 | 9 | const app = new App({ 10 | applicationId: 'integration', 11 | https: false, 12 | host: 'localhost:1337/parse', 13 | httpController: HttpController, 14 | }); 15 | 16 | const rawClient = new Client({ 17 | https: false, 18 | host: 'localhost:1337', 19 | httpController: HttpController, 20 | }); 21 | 22 | suite('Query', () => { 23 | beforeEach((done) => { 24 | clearApps(rawClient).then(() => { 25 | done(); 26 | }, (err) => { 27 | console.log('Clear App Error:', err); 28 | }); 29 | }); 30 | 31 | test('getting a single object', (done) => { 32 | let id; 33 | Save(app, 'QueryObject', {}).then((o) => { 34 | id = o.objectId; 35 | return Query.get(app, 'QueryObject', id); 36 | }).then((o) => { 37 | expect(o.objectId).to.equal(id); 38 | expect(o.createdAt).to.exist; 39 | done(); 40 | }).catch(err => console.log(err)); 41 | }); 42 | 43 | test('getting objects by equality', (done) => { 44 | Promise.all([ 45 | Save(app, 'QueryObject', {draft: true, score: 10}), 46 | Save(app, 'QueryObject', {draft: false, score: 20}), 47 | Save(app, 'QueryObject', {draft: true, score: 30}), 48 | ]).then(() => { 49 | const query = Query.equalTo({}, 'draft', true); 50 | return Query.find(app, 'QueryObject', query); 51 | }).then((objs) => { 52 | expect(objs.length).to.equal(2); 53 | const query = Query.equalTo({}, 'draft', false); 54 | return Query.find(app, 'QueryObject', query); 55 | }).then((objs) => { 56 | expect(objs.length).to.equal(1); 57 | expect(objs[0].score).to.equal(20); 58 | done(); 59 | }).catch(e => console.log(e)); 60 | }); 61 | 62 | test('getting objects by keys in an array', (done) => { 63 | Promise.all([ 64 | Save(app, 'QueryObject', {tags: ['a', 'b']}), 65 | Save(app, 'QueryObject', {tags: ['b', 'c']}), 66 | Save(app, 'QueryObject', {tags: ['a', 'c']}), 67 | ]).then(() => { 68 | const query = Query.equalTo({}, 'tags', 'b'); 69 | return Query.find(app, 'QueryObject', query); 70 | }).then((objs) => { 71 | expect(objs.length).to.equal(2); 72 | if (objs[0].tags[0] === 'a') { 73 | expect(objs[0].tags).to.deep.equal(['a', 'b']); 74 | expect(objs[1].tags).to.deep.equal(['b', 'c']); 75 | } else { 76 | expect(objs[0].tags).to.deep.equal(['b', 'c']); 77 | expect(objs[1].tags).to.deep.equal(['a', 'b']); 78 | } 79 | done(); 80 | }).catch(e => console.log(e)); 81 | }); 82 | }); 83 | -------------------------------------------------------------------------------- /integration/test/SaveTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import clearApps from './clearApps'; 4 | import {expect} from 'chai'; 5 | import {App, Ops, Query, Save} from '../../index'; 6 | import {Client} from 'ibeam'; 7 | import HttpController from 'ibeam/http-node'; 8 | 9 | const app = new App({ 10 | applicationId: 'integration', 11 | https: false, 12 | host: 'localhost:1337/parse', 13 | httpController: HttpController, 14 | }); 15 | 16 | const rawClient = new Client({ 17 | https: false, 18 | host: 'localhost:1337', 19 | httpController: HttpController, 20 | }); 21 | 22 | suite('Save', () => { 23 | beforeEach((done) => { 24 | clearApps(rawClient).then(() => { 25 | done(); 26 | }, (err) => { 27 | console.log('Clear App Error:', err); 28 | }); 29 | }); 30 | 31 | test('saving returns an object id', (done) => { 32 | Save(app, 'TestObject', {}).then((o) => { 33 | expect(o.objectId).to.exist; 34 | done(); 35 | }).catch(err => console.log(err)); 36 | }); 37 | 38 | test('saving attaches hidden fields', (done) => { 39 | Save(app, 'TestObject', {count: 5}).then((o) => { 40 | expect(o.count).to.equal(5); 41 | expect(o._serverData.count).to.equal(5); 42 | expect(o._className).to.equal('TestObject'); 43 | expect(o._opSet).to.deep.equal({}); 44 | done(); 45 | }).catch(err => console.log(err)); 46 | }); 47 | 48 | test('saving updates an existing object, immutably', (done) => { 49 | let obj = null; 50 | Save(app, 'TestObject', {count: 10}).then((o) => { 51 | obj = o; 52 | return Save(app, 'TestObject', {objectId: o.objectId, foo: 'bar'}); 53 | }).then((o) => { 54 | expect(o.objectId).to.equal(obj.objectId); 55 | expect(o.foo).to.equal('bar'); 56 | expect(o.count).to.not.exist; 57 | expect(o).to.not.equal(obj); 58 | done(); 59 | }).catch(err => console.log(err)); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /integration/test/UserTest.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import clearApps from './clearApps'; 4 | import {expect} from 'chai'; 5 | import {App, Ops, Query, Save, User} from '../../index'; 6 | import {Client} from 'ibeam'; 7 | import HttpController from 'ibeam/http-node'; 8 | 9 | const app = new App({ 10 | applicationId: 'integration', 11 | https: false, 12 | host: 'localhost:1337/parse', 13 | httpController: HttpController, 14 | }); 15 | 16 | const rawClient = new Client({ 17 | https: false, 18 | host: 'localhost:1337', 19 | httpController: HttpController, 20 | }); 21 | 22 | suite('User', () => { 23 | beforeEach((done) => { 24 | clearApps(rawClient).then(() => { 25 | done(); 26 | }, (err) => { 27 | console.log('Clear App Error:', err); 28 | }); 29 | }); 30 | 31 | test('signing up', (done) => { 32 | User.signUp(app, { 33 | username: 'andrew', 34 | password: 's3cret', 35 | }).then(({sessionToken, user}) => { 36 | expect(sessionToken).to.exist; 37 | expect(user.objectId).to.exist; 38 | expect(user.username).to.equal('andrew'); 39 | expect(user.password).to.not.exist; 40 | expect(user.sessionToken).to.not.exist; 41 | done(); 42 | }).catch(e => console.log(e)); 43 | }); 44 | 45 | test('signing up with email', (done) => { 46 | User.signUp(app, { 47 | username: 'andrew', 48 | email: 'a@example.com', 49 | password: 's3cret', 50 | }).then(({sessionToken, user}) => { 51 | expect(sessionToken).to.exist; 52 | expect(user.objectId).to.exist; 53 | expect(user.username).to.equal('andrew'); 54 | expect(user.email).to.equal('a@example.com'); 55 | done(); 56 | }).catch(e => console.log(e)); 57 | }); 58 | 59 | test('modifying a user', (done) => { 60 | let token, u; 61 | User.signUp(app, { 62 | username: 'andrew', 63 | password: 's3cret', 64 | }).then(({sessionToken, user}) => { 65 | token = sessionToken; 66 | u = user; 67 | return Save(app, '_User', Ops.set(user, {age: 27})); 68 | }).then(() => { 69 | // Expect an error 70 | throw new Error('Save should fail'); 71 | }, () => { 72 | return Save(app, '_User', Ops.set(u, {age: 28}), {sessionToken: token}); 73 | }).then((user) => { 74 | expect(user.username).to.equal('andrew'); 75 | expect(user.age).to.equal(28); 76 | done(); 77 | }).catch(e => console.log(e)); 78 | }); 79 | 80 | test('logging in', (done) => { 81 | User.signUp(app, {username: 'andrew', password: 's3cret'}).then(() => { 82 | return User.logIn(app, {username: 'andrew', password: 'wrong'}); 83 | }).then(() => { 84 | throw new Error('First login should fail'); 85 | }, () => { 86 | return User.logIn(app, {username: 'andrew', password: 's3cret'}); 87 | }).then(({sessionToken, user}) => { 88 | expect(user.username).to.equal('andrew'); 89 | done(); 90 | }).catch(e => console.log(e)); 91 | }); 92 | 93 | test('user has server fields', (done) => { 94 | let m; 95 | User.signUp(app, {username: 'andrew', password: 's3cret'}).then(({user}) => { 96 | expect(user._serverData).to.exist; 97 | expect(user._serverData.username).to.equal('andrew'); 98 | expect(user._serverData.createdAt).to.exist; 99 | const modified = Ops.set(user, {score: 12}); 100 | expect(user.score).to.equal(undefined); 101 | expect(modified.score).to.equal(12); 102 | expect(modified._serverData.score).to.equal(undefined); 103 | expect(modified._opSet).to.deep.equal({score: 12}); 104 | return User.logIn(app, {username: 'andrew', password: 's3cret'}); 105 | }).then(({user, sessionToken}) => { 106 | expect(user._serverData).to.exist; 107 | expect(user._serverData.username).to.equal('andrew'); 108 | expect(user._serverData.createdAt).to.exist; 109 | expect(user._serverData.updatedAt).to.exist; 110 | const modified = Ops.set(user, {score: 12}); 111 | expect(user.score).to.equal(undefined); 112 | expect(modified.score).to.equal(12); 113 | expect(modified._serverData.score).to.equal(undefined); 114 | expect(modified._opSet).to.deep.equal({score: 12}); 115 | m = modified; 116 | return Save(app, '_User', modified, {sessionToken}); 117 | }).then((user) => { 118 | expect(user.score).to.equal(12); 119 | expect(user._serverData.score).to.equal(12); 120 | expect(user._opSet).to.deep.equal({}); 121 | expect(m.score).to.equal(12); 122 | expect(m._serverData.score).to.equal(undefined); 123 | expect(m._opSet.score).to.equal(12); 124 | done(); 125 | }).catch(e => console.log(e)); 126 | }); 127 | 128 | test('getting a user for a session token', (done) => { 129 | User.signUp(app, { 130 | username: 'andrew', 131 | password: 's3cret' 132 | }).then(({sessionToken}) => { 133 | return User.forSession(app, sessionToken); 134 | }).then((user) => { 135 | expect(user.username).to.equal('andrew'); 136 | done(); 137 | }).catch(e => console.log(e)); 138 | }); 139 | 140 | test('logging out', (done) => { 141 | let token; 142 | User.signUp(app, { 143 | username: 'andrew', 144 | password: 's3cret', 145 | }).then(({sessionToken}) => { 146 | token = sessionToken; 147 | return User.logOut(app, sessionToken); 148 | }).then(() => { 149 | return User.forSession(app, token); 150 | }).then(() => { 151 | throw new Error('Finding user should fail'); 152 | }, (e) => { 153 | done(); 154 | }).catch(e => console.log(e)); 155 | }); 156 | }); 157 | -------------------------------------------------------------------------------- /integration/test/clearApps.js: -------------------------------------------------------------------------------- 1 | module.exports = function(client) { 2 | return client.get('clear', '', {host: 'localhost:1337'}); 3 | }; 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "parse-lite", 3 | "version": "0.2.0", 4 | "description": "The universal JS library for Parse Server", 5 | "homepage": "https://github.com/andrewimm/parse-lite", 6 | "license": "BSD-3-Clause", 7 | "repository": { 8 | "type": "git", 9 | "url": "https://github.com/andrewimm/parse-lite" 10 | }, 11 | "dependencies": { 12 | "ibeam": "^0.1.1" 13 | }, 14 | "files": [ 15 | "lib", 16 | "index.js", 17 | "README.md" 18 | ], 19 | "devDependencies": { 20 | "babel-cli": "^6.14.0", 21 | "babel-plugin-syntax-flow": "^6.3.13", 22 | "babel-plugin-transform-flow-strip-types": "^6.3.15", 23 | "babel-preset-es2015": "^6.3.13", 24 | "babel-preset-stage-1": "^6.3.0", 25 | "babel-register": "^6.5.2", 26 | "chai": "^3.5.0", 27 | "mocha": "^2.4.5" 28 | }, 29 | "scripts": { 30 | "build": "babel src --out-dir lib --copy-files --ignore defs", 31 | "test": "mocha --ui tdd --compilers js:babel-register" 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /run_integration.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | 3 | npm run build 4 | cd integration 5 | npm install 6 | TESTING=1 node server.js & 7 | PID=$! 8 | npm test 9 | C=$? 10 | kill -9 $PID 11 | exit $C 12 | -------------------------------------------------------------------------------- /src/.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | .*/node_modules/.* 3 | 4 | [include] 5 | 6 | [libs] 7 | defs/ 8 | 9 | [options] 10 | -------------------------------------------------------------------------------- /src/App.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import { 4 | Client, 5 | addRequestPreProcessor, 6 | addResponsePostProcessor 7 | } from 'ibeam'; 8 | 9 | import type { 10 | AuthOptions, 11 | } from './Types'; 12 | 13 | export default class App { 14 | _ApplicationId: string; 15 | _JavaScriptKey: ?string; 16 | _MasterKey: ?string; 17 | _bareClient: Client; 18 | client: Client; 19 | 20 | constructor(options: Object = {}) { 21 | const clientOpts: Object = { 22 | host: options.host, 23 | format: 'json', 24 | }; 25 | if (options.hasOwnProperty('https')) { 26 | clientOpts.https = options.https; 27 | } 28 | if (options.hasOwnProperty('httpController')) { 29 | clientOpts.httpController = options.httpController; 30 | } 31 | this._ApplicationId = options.applicationId; 32 | this._JavaScriptKey = options.javaScriptKey; 33 | this._MasterKey = options.masterKey; 34 | this._bareClient = new Client(clientOpts); 35 | this.client = addResponsePostProcessor( 36 | addRequestPreProcessor(this._bareClient, this.requestPreProcessor.bind(this)), 37 | this.responsePostProcessor 38 | ); 39 | } 40 | 41 | requestPreProcessor(method: string, path: string, payload: any, options: AuthOptions) { 42 | const json: Object = { 43 | _ApplicationId: this._ApplicationId, 44 | }; 45 | if (this._JavaScriptKey) { 46 | json._JavaScriptKey = this._JavaScriptKey; 47 | } 48 | if (payload && typeof payload === 'object') { 49 | for (let key in payload) { 50 | json[key] = payload[key]; 51 | } 52 | } 53 | if (options.useMasterKey) { 54 | if (this._MasterKey) { 55 | json._MasterKey = this._MasterKey; 56 | } else { 57 | throw new Error('Cannot use the master key. It has not been provided.'); 58 | } 59 | } 60 | if (options.sessionToken) { 61 | json._SessionToken = options.sessionToken; 62 | } 63 | json._method = method; 64 | const clientOpts = { 65 | headers: { 66 | 'Content-Type': 'text/plain', 67 | }, 68 | }; 69 | for (let key in options) { 70 | if (key !== 'sessionToken' && key !== 'useMasterKey') { 71 | clientOpts[key] = options[key]; 72 | } 73 | } 74 | 75 | return { 76 | method: 'POST', 77 | payload: json, 78 | options: clientOpts, 79 | }; 80 | } 81 | 82 | responsePostProcessor( 83 | response: {status: number, response: string} 84 | ): Promise<{status: number, response: Object}> { 85 | let body = null; 86 | try { 87 | body = JSON.parse(response.response); 88 | } catch (e) { 89 | return Promise.reject({ 90 | status: response.status, 91 | response: `Could not parse JSON response: "${response.response}"`, 92 | }); 93 | } 94 | if (response.status >= 200 && response.status < 300) { 95 | return Promise.resolve({ 96 | status: response.status, 97 | response: body, 98 | }); 99 | } 100 | return Promise.reject({ 101 | status: response.status, 102 | response: body, 103 | }); 104 | } 105 | } 106 | -------------------------------------------------------------------------------- /src/Cloud.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {decode, encode} from './WireFormat'; 4 | 5 | import type App from './App'; 6 | import type {AuthOptions} from './Types'; 7 | 8 | type CloudResponse = { 9 | result?: any, 10 | }; 11 | 12 | export default function Cloud( 13 | app: App, 14 | name: string, 15 | data: any = {}, 16 | options: AuthOptions = {} 17 | ): Promise { 18 | if (typeof name !== 'string' || name.length === 0) { 19 | throw new TypeError('Cloud function name must be a string.'); 20 | } 21 | 22 | const payload = encode(data); 23 | 24 | return app.client.post( 25 | 'functions/' + name, 26 | payload, 27 | options 28 | ).then(({response}) => { 29 | const decoded: CloudResponse = (decode(response): any); 30 | if (decoded && decoded.hasOwnProperty('result')) { 31 | return Promise.resolve(decoded.result); 32 | } 33 | return Promise.reject('The server returned an invalid response.'); 34 | }).catch(({response}) => { 35 | return Promise.reject(response); 36 | }); 37 | } 38 | -------------------------------------------------------------------------------- /src/Date.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | const toString = Object.prototype.toString; 4 | 5 | export function isDate(obj: any): boolean { 6 | return toString.call(obj) === '[object Date]'; 7 | } 8 | -------------------------------------------------------------------------------- /src/Destroy.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import type App from './App'; 4 | 5 | import type {Identifier} from './Types'; 6 | import type {AuthOptions} from './Types'; 7 | 8 | export default function Destroy( 9 | app: App, 10 | className: string, 11 | object: Identifier, 12 | options: AuthOptions = {} 13 | ): Promise { 14 | if (!object || (typeof object !== 'object' && typeof object !== 'string')) { 15 | return Promise.reject(new Error('Cannot destroy an invalid object')); 16 | } 17 | if (typeof object === 'object' && !object.objectId) { 18 | return Promise.reject(new Error('Cannot destroy an unsaved object')); 19 | } 20 | let id = typeof object === 'string' ? object : object.objectId; 21 | 22 | return app.client.delete( 23 | `classes/${className}/${id}`, 24 | null, 25 | options 26 | ).then(({response}) => { 27 | return response; 28 | }); 29 | } 30 | -------------------------------------------------------------------------------- /src/Installation.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | function hexOctet(): string { 4 | return Math.floor( 5 | (1 + Math.random()) * 0x10000 6 | ).toString(16).substring(1); 7 | } 8 | 9 | export const CLASS_NAME = '_Installation'; 10 | 11 | export function generateId(): string { 12 | return ( 13 | hexOctet() + hexOctet() + '-' + 14 | hexOctet() + '-' + 15 | hexOctet() + '-' + 16 | hexOctet() + '-' + 17 | hexOctet() + hexOctet() + hexOctet() 18 | ); 19 | } 20 | -------------------------------------------------------------------------------- /src/Ops.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import arrayValueMatches from './arrayValueMatches'; 4 | import deepCopy from './deepCopy'; 5 | 6 | import type {Op, OpSet} from './Types'; 7 | 8 | type AttributeMap = {[key: string]: any}; 9 | 10 | /** 11 | * Set keys to specific values, or apply raw Ops 12 | */ 13 | export function set(obj: AttributeMap, changes: OpSet): AttributeMap { 14 | const result = {}; 15 | const serverData = obj._serverData || {}; 16 | for (let key in serverData) { 17 | result[key] = serverData[key]; 18 | } 19 | Object.defineProperty(result, '_serverData', { 20 | enumerable: false, 21 | writable: false, 22 | configurable: false, 23 | value: serverData 24 | }); 25 | 26 | const opSet = obj._opSet || {}; 27 | let existing = false; 28 | for (let key in obj) { 29 | result[key] = obj._serverData && obj._serverData.hasOwnProperty(key) ? 30 | obj._serverData[key] : 31 | obj[key]; 32 | if (key === 'objectId') { 33 | existing = true; 34 | } else if (!obj._opSet && !obj._serverData) { 35 | opSet[key] = obj[key]; 36 | } 37 | } 38 | 39 | for (let key in changes) { 40 | let op = changes[key]; 41 | let source; 42 | if (op && op.__op) { 43 | switch (op.__op) { 44 | case 'Delete': 45 | delete result[key]; 46 | if (existing) { 47 | opSet[key] = op; 48 | } else { 49 | delete opSet[key]; 50 | } 51 | break; 52 | case 'Increment': 53 | let amount = isNaN(op.amount) ? 1 : op.amount; 54 | result[key] = (result[key] || 0) + amount; 55 | if (existing) { 56 | opSet[key] = op; 57 | } else { 58 | opSet[key] = result[key]; 59 | } 60 | break; 61 | case 'Add': 62 | let toAdd = deepCopy( 63 | Array.isArray(op.objects) ? op.objects : [op.objects] 64 | ); 65 | result[key] = (result[key] || []).concat(toAdd); 66 | if (existing) { 67 | opSet[key] = op; 68 | } else { 69 | opSet[key] = result[key]; 70 | } 71 | break; 72 | case 'Remove': 73 | let toRemove = Array.isArray(op.objects) ? op.objects : [op.objects]; 74 | let removed = []; 75 | source = result[key] || []; 76 | for (let i = 0; i < source.length; i++) { 77 | let matched = false; 78 | for (let j = 0; j < toRemove.length; j++) { 79 | if (matched) { 80 | continue; 81 | } 82 | if (arrayValueMatches(source[i], toRemove[j])) { 83 | matched = true; 84 | } 85 | } 86 | if (!matched) { 87 | removed.push(source[i]); 88 | } 89 | } 90 | result[key] = removed; 91 | if (existing) { 92 | opSet[key] = op; 93 | } else { 94 | opSet[key] = result[key]; 95 | } 96 | break; 97 | case 'AddUnique': 98 | let toAddUnique = Array.isArray(op.objects) ? op.objects : [op.objects]; 99 | let uniqueAdds = []; 100 | source = result[key] || []; 101 | for (let i = 0; i < toAddUnique.length; i++) { 102 | let matched = false; 103 | for (let j = 0; j < source.length; j++) { 104 | if (matched) { 105 | continue; 106 | } 107 | if (arrayValueMatches(toAddUnique[i], source[j])) { 108 | matched = true; 109 | } 110 | } 111 | if (!matched) { 112 | uniqueAdds.push(deepCopy(toAddUnique[i])); 113 | } 114 | } 115 | let merged = source.concat(uniqueAdds); 116 | result[key] = merged; 117 | if (existing) { 118 | opSet[key] = op; 119 | } else { 120 | opSet[key] = result[key]; 121 | } 122 | break; 123 | default: 124 | throw new Error('Unsupported Op'); 125 | } 126 | } else { 127 | // Standard set op 128 | result[key] = deepCopy(op); 129 | opSet[key] = deepCopy(op); 130 | } 131 | } 132 | Object.defineProperty(result, '_opSet', { 133 | enumerable: false, 134 | writable: false, 135 | configurable: false, 136 | value: opSet 137 | }); 138 | 139 | return result; 140 | } 141 | 142 | /** 143 | * Remove a field from an object 144 | */ 145 | export function unset(obj: AttributeMap, key: string): AttributeMap { 146 | return set(obj, { [key]: { __op: 'Delete' } }); 147 | } 148 | 149 | /** 150 | * Atomically increment (or decrement) a numeric field 151 | */ 152 | export function increment(obj: AttributeMap, key: string, amount?: number): AttributeMap { 153 | const inc = typeof amount === 'number' ? amount : 1; 154 | return set(obj, { [key]: { __op: 'Increment', amount: inc } }); 155 | } 156 | 157 | /** 158 | * Atomically add elements to an Array field 159 | */ 160 | export function add(obj: AttributeMap, key: string, objects: any): AttributeMap { 161 | if (!Array.isArray(objects)) { 162 | objects = [objects]; 163 | } 164 | return set(obj, { [key]: { __op: 'Add', objects: objects } }); 165 | } 166 | 167 | /** 168 | * Atomically add elements to an Array field, if they don't already exist 169 | */ 170 | export function addUnique(obj: AttributeMap, key: string, objects: any): AttributeMap { 171 | if (!Array.isArray(objects)) { 172 | objects = [objects]; 173 | } 174 | return set(obj, { [key]: { __op: 'AddUnique', objects: objects } }); 175 | } 176 | 177 | /** 178 | * Atomically remove elements from an Array field 179 | */ 180 | export function remove(obj: AttributeMap, key: string, objects: any): AttributeMap { 181 | if (!Array.isArray(objects)) { 182 | objects = [objects]; 183 | } 184 | return set(obj, { [key]: { __op: 'Remove', objects: objects } }); 185 | } 186 | 187 | /** 188 | * Client-side operation to revert all unsaved changes 189 | */ 190 | export function revert(obj: AttributeMap, key: string): AttributeMap { 191 | if (!obj._serverData) { 192 | return {}; 193 | } 194 | const original = {}; 195 | Object.defineProperty(original, '_serverData', { 196 | enumerable: false, 197 | writable: false, 198 | configurable: false, 199 | value: obj._serverData 200 | }); 201 | Object.defineProperty(original, '_opSet', { 202 | enumerable: false, 203 | writable: false, 204 | configurable: false, 205 | value: {} 206 | }); 207 | for (let key in obj._serverData) { 208 | original[key] = obj._serverData[key]; 209 | } 210 | return original; 211 | } 212 | -------------------------------------------------------------------------------- /src/Query.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {decode, encode} from './WireFormat'; 4 | import deepCopy from './deepCopy'; 5 | 6 | import type App from './App'; 7 | import type { 8 | AuthOptions, 9 | Comparable, 10 | GeoPoint, 11 | ParseObject, 12 | ParseValue, 13 | ParseVector, 14 | } from './Types'; 15 | 16 | type WhereClause = { 17 | [attr: string]: any; 18 | }; 19 | 20 | type QueryJSON = { 21 | where: WhereClause; 22 | include: Array; 23 | keys: ?Array; 24 | limit: number; 25 | skip: number; 26 | order: Array; 27 | count?: number; 28 | className?: string; 29 | }; 30 | 31 | const CONSTRAINTS = { 32 | $ne: true, 33 | $lt: true, 34 | $lte: true, 35 | $gt: true, 36 | $gte: true, 37 | $in: true, 38 | $nin: true, 39 | $all: true, 40 | $exists: true, 41 | $regex: true, 42 | $options: true, 43 | $inQuery: true, 44 | $notInQuery: true, 45 | $select: true, 46 | $dontSelect: true, 47 | $nearSphere: true, 48 | $within: true, 49 | }; 50 | 51 | function isConstraint(value: Object): boolean { 52 | for (let c in CONSTRAINTS) { 53 | if (value.hasOwnProperty(c)) { 54 | return true; 55 | } 56 | } 57 | return false; 58 | } 59 | 60 | function quote(s: string): string { 61 | return '\\Q' + s.replace('\\E', '\\E\\\\E\\Q') + '\\E'; 62 | } 63 | 64 | export function emptyQuery(): QueryJSON { 65 | return { 66 | where: {}, 67 | order: [], 68 | limit: -1, 69 | skip: 0, 70 | keys: null, 71 | include: [], 72 | }; 73 | } 74 | 75 | function copyQuery(q: QueryJSON): QueryJSON { 76 | const copy = { 77 | where: q.where ? deepCopy(q.where) : {}, 78 | order: q.order ? [].concat(q.order) : [], 79 | limit: q.hasOwnProperty('limit') ? q.limit : -1, 80 | skip: q.skip || 0, 81 | keys: q.keys ? [].concat(q.keys) : null, 82 | include: q.include ? [].concat(q.include) : [], 83 | }; 84 | return copy; 85 | } 86 | 87 | function copyWithNewConstraint( 88 | q: QueryJSON, 89 | field: string, 90 | constraint: string, 91 | value: any 92 | ): QueryJSON { 93 | const copy = copyQuery(q); 94 | const encoded = encode(value, true); 95 | if (copy.where.hasOwnProperty(field)) { 96 | if (!isConstraint(copy.where[field])) { 97 | copy.where[field] = {}; 98 | } 99 | } else { 100 | copy.where[field] = {}; 101 | } 102 | copy.where[field][constraint] = encoded; 103 | return copy; 104 | } 105 | 106 | export function equalTo(q: QueryJSON, field: string, value: any): QueryJSON { 107 | if (typeof value === 'undefined') { 108 | return doesNotExist(q, field); 109 | } 110 | const copy = copyQuery(q); 111 | copy.where[field] = encode(value); 112 | return copy; 113 | } 114 | 115 | export function notEqualTo(q: QueryJSON, field: string, value: any): QueryJSON { 116 | return copyWithNewConstraint(q, field, '$ne', value); 117 | } 118 | 119 | export function lessThan(q: QueryJSON, field: string, value: Comparable): QueryJSON { 120 | return copyWithNewConstraint(q, field, '$lt', value); 121 | } 122 | 123 | export function lessThanOrEqualTo(q: QueryJSON, field: string, value: Comparable): QueryJSON { 124 | return copyWithNewConstraint(q, field, '$lte', value); 125 | } 126 | 127 | export function greaterThan(q: QueryJSON, field: string, value: Comparable): QueryJSON { 128 | return copyWithNewConstraint(q, field, '$gt', value); 129 | } 130 | 131 | export function greaterThanOrEqualTo(q: QueryJSON, field: string, value: Comparable): QueryJSON { 132 | return copyWithNewConstraint(q, field, '$gte', value); 133 | } 134 | 135 | export function containedIn(q: QueryJSON, field: string, value: any): QueryJSON { 136 | return copyWithNewConstraint(q, field, '$in', value); 137 | } 138 | 139 | export function notContainedIn(q: QueryJSON, field: string, value: any): QueryJSON { 140 | return copyWithNewConstraint(q, field, '$nin', value); 141 | } 142 | 143 | export function containsAll(q: QueryJSON, field: string, value: any): QueryJSON { 144 | return copyWithNewConstraint(q, field, '$all', value); 145 | } 146 | 147 | export function exists(q: QueryJSON, field: string): QueryJSON { 148 | return copyWithNewConstraint(q, field, '$exists', true); 149 | } 150 | 151 | export function doesNotExist(q: QueryJSON, field: string): QueryJSON { 152 | return copyWithNewConstraint(q, field, '$exists', false); 153 | } 154 | 155 | export function matches(q: QueryJSON, field: string, regex: RegExp, modifiers: string = ''): QueryJSON { 156 | const copy = copyWithNewConstraint(q, field, '$regex', regex); 157 | if (regex.ignoreCase) { 158 | modifiers += 'i'; 159 | } 160 | if (regex.multiline) { 161 | modifiers += 'm'; 162 | } 163 | if (modifiers.length) { 164 | copy.where[field].$options = modifiers; 165 | } 166 | return copy; 167 | } 168 | 169 | export function matchesQuery(q: QueryJSON, field: string, className: string, query: QueryJSON): QueryJSON { 170 | return copyWithNewConstraint(q, field, '$inQuery', {...query, className}); 171 | } 172 | 173 | export function doesNotMatchQuery(q: QueryJSON, field: string, className: string, query: QueryJSON): QueryJSON { 174 | return copyWithNewConstraint(q, field, '$notInQuery', {...query, className}); 175 | } 176 | 177 | export function matchesKeyInQuery(q: QueryJSON, field: string, className: string, query: QueryJSON, queryKey: string): QueryJSON { 178 | return copyWithNewConstraint(q, field, '$select', { 179 | key: queryKey, 180 | query: {...query, className}, 181 | }); 182 | } 183 | 184 | export function doesNotMatchKeyInQuery(q: QueryJSON, field: string, className: string, query: QueryJSON, queryKey: string): QueryJSON { 185 | return copyWithNewConstraint(q, field, '$dontSelect', { 186 | key: queryKey, 187 | query: {...query, className}, 188 | }); 189 | } 190 | 191 | export function contains(q: QueryJSON, field: string, value: string): QueryJSON { 192 | if (typeof value !== 'string') { 193 | throw new Error('The value being searched for must be a string.'); 194 | } 195 | return copyWithNewConstraint(q, field, '$regex', quote(value)); 196 | } 197 | 198 | export function startsWith(q: QueryJSON, field: string, value: string): QueryJSON { 199 | if (typeof value !== 'string') { 200 | throw new Error('The value being searched for must be a string.'); 201 | } 202 | return copyWithNewConstraint(q, field, '$regex', '^' + quote(value)); 203 | } 204 | 205 | export function endsWith(q: QueryJSON, field: string, value: string): QueryJSON { 206 | if (typeof value !== 'string') { 207 | throw new Error('The value being searched for must be a string.'); 208 | } 209 | return copyWithNewConstraint(q, field, '$regex', quote(value) + '$'); 210 | } 211 | 212 | export function near(q: QueryJSON, field: string, point: GeoPoint): QueryJSON { 213 | return copyWithNewConstraint(q, field, '$nearSphere', point); 214 | } 215 | 216 | export function withinRadians(q: QueryJSON, field: string, point: GeoPoint, distance: number): QueryJSON { 217 | const copy = copyWithNewConstraint(q, field, '$nearSphere', point); 218 | copy.where[field].$maxDistance = distance; 219 | return copy; 220 | } 221 | 222 | export function withinMiles(q: QueryJSON, field: string, point: GeoPoint, distance: number): QueryJSON { 223 | return withinRadians(q, field, point, distance / 3958.8); 224 | } 225 | 226 | export function withinKilometers(q: QueryJSON, field: string, point: GeoPoint, distance: number): QueryJSON { 227 | return withinRadians(q, field, point, distance / 6371.0); 228 | } 229 | 230 | export function withinGeoBox(q: QueryJSON, field: string, southwest: GeoPoint, northeast: GeoPoint): QueryJSON { 231 | return copyWithNewConstraint(q, field, '$within', { 232 | $box: [southwest, northeast] 233 | }); 234 | } 235 | 236 | export function ascending(q: QueryJSON, ...keys: Array): QueryJSON { 237 | const copy = copyQuery(q); 238 | copy.order = keys.map((k) => { 239 | if (typeof k !== 'string') { 240 | throw new Error('Ascending should only take strings as secondary arguments'); 241 | } 242 | return k; 243 | }); 244 | return copy; 245 | } 246 | 247 | export function descending(q: QueryJSON, ...keys: Array): QueryJSON { 248 | const copy = copyQuery(q); 249 | copy.order = keys.map((k) => { 250 | if (typeof k !== 'string') { 251 | throw new Error('Descending should only take strings as secondary arguments'); 252 | } 253 | return '-' + k; 254 | }); 255 | return copy; 256 | } 257 | 258 | export function skip(q: QueryJSON, n: number): QueryJSON { 259 | if (typeof n !== 'number' || n < 0) { 260 | throw new Error('You can only skip by a positive number'); 261 | } 262 | const copy = copyQuery(q); 263 | copy.skip = n; 264 | return copy; 265 | } 266 | 267 | export function limit(q: QueryJSON, n: number): QueryJSON { 268 | if (typeof n !== 'number' || n < 0) { 269 | throw new Error('You can only skip by a positive number'); 270 | } 271 | const copy = copyQuery(q); 272 | copy.limit = n; 273 | return copy; 274 | } 275 | 276 | export function include(q: QueryJSON, ...keys: Array): QueryJSON { 277 | const copy = copyQuery(q); 278 | copy.include = keys; 279 | return copy; 280 | } 281 | 282 | export function select(q: QueryJSON, ...keys: Array): QueryJSON { 283 | throw new Error('not yet implemented'); 284 | } 285 | 286 | export function or(...queries: Array): QueryJSON { 287 | if (queries.length === 0) { 288 | throw new Error('At least one input query is required'); 289 | } 290 | 291 | let q = emptyQuery(); 292 | q.where = { 293 | $or: [queries.map((q) => deepCopy(q.where))] 294 | }; 295 | return q; 296 | } 297 | 298 | export function find( 299 | app: App, 300 | className: string, 301 | q: QueryJSON, 302 | options: AuthOptions = {} 303 | ): Promise> { 304 | const filtered: Object = { 305 | where: q.where, 306 | }; 307 | if (q.order && q.order.length) { 308 | filtered.order = q.order.join(','); 309 | } 310 | if (q.limit > -1) { 311 | filtered.limit = q.limit; 312 | } 313 | if (q.skip > 0) { 314 | filtered.skip = q.skip; 315 | } 316 | if (q.keys && q.keys.length) { 317 | filtered.keys = q.keys.join(','); 318 | } 319 | if (q.include && q.include.length) { 320 | filtered.include = q.include.join(','); 321 | } 322 | return app.client.get( 323 | `classes/${className}`, 324 | filtered, 325 | options 326 | ).then((response) => { 327 | const results = response.response.results; 328 | const objects = results.map((r) => { 329 | if (!r) { 330 | return r; 331 | } 332 | const obj: ParseObject = (decode(r): any); 333 | Object.defineProperty(obj, '_serverData', { 334 | enumerable: false, 335 | writeable: false, 336 | configurable: false, 337 | value: (decode(r): any), 338 | }); 339 | Object.defineProperty(obj, '_className', { 340 | enumerable: false, 341 | writeable: false, 342 | configurable: false, 343 | value: className, 344 | }); 345 | return obj; 346 | }); 347 | return objects; 348 | }); 349 | } 350 | 351 | export function get( 352 | app: App, 353 | className: string, 354 | objectId: string, 355 | options: AuthOptions = {} 356 | ): Promise { 357 | return app.client.get( 358 | `classes/${className}/${objectId}`, 359 | {}, 360 | options 361 | ).then((response) => { 362 | const result = response.response; 363 | if (!result) { 364 | return result; 365 | } 366 | const obj: ParseObject = (decode(result): any); 367 | Object.defineProperty(obj, '_serverData', { 368 | enumerable: false, 369 | writeable: false, 370 | configurable: false, 371 | value: (decode(result): any), 372 | }); 373 | Object.defineProperty(obj, '_className', { 374 | enumerable: false, 375 | writeable: false, 376 | configurable: false, 377 | value: className, 378 | }); 379 | return obj; 380 | }); 381 | }; 382 | -------------------------------------------------------------------------------- /src/Save.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import {decode, encode} from './WireFormat'; 4 | import deepCopy from './deepCopy'; 5 | import * as Ops from './Ops'; 6 | 7 | import type App from './App'; 8 | import type {AuthOptions, ParseObject} from './Types'; 9 | 10 | export default function Save( 11 | app: App, 12 | className: string, 13 | object: ParseObject, 14 | options: AuthOptions = {} 15 | ): Promise { 16 | if (!object || typeof object !== 'object') { 17 | return Promise.reject(new Error('Cannot save an invalid object')); 18 | } 19 | if (!object._opSet) { 20 | object = Ops.set(object, {}); 21 | } 22 | const method = object.objectId ? 'PUT' : 'POST'; 23 | if (object.objectId && typeof object.objectId !== 'string') { 24 | throw new Error('Invalid id: objectId field must be a string'); 25 | } 26 | const path = object.objectId ? 27 | `classes/${className}/${object.objectId}` : 28 | `classes/${className}`; 29 | return app.client.raw( 30 | method, 31 | path, 32 | encode(object._opSet), 33 | options 34 | ).then((response) => { 35 | const result = response.response; 36 | const savedServerData = {}; 37 | const setFields = {}; 38 | for (let key in object._opSet) { 39 | if (!object._opSet[key] || !object._opSet[key].__op) { 40 | setFields[key] = object._opSet[key]; 41 | } 42 | } 43 | if (object.objectId) { 44 | setFields.objectId = object.objectId; 45 | } 46 | Object.assign( 47 | savedServerData, 48 | deepCopy(object._serverData || {}), 49 | setFields, 50 | decode(result) 51 | ); 52 | const savedObject = {}; 53 | Object.assign( 54 | savedObject, 55 | deepCopy(object._serverData || {}), 56 | setFields, 57 | decode(result) 58 | ); 59 | Object.defineProperty(savedObject, '_serverData', { 60 | enumerable: false, 61 | writable: false, 62 | configurable: false, 63 | value: savedServerData, 64 | }); 65 | Object.defineProperty(savedObject, '_className', { 66 | enumerable: false, 67 | writable: false, 68 | configurable: false, 69 | value: className, 70 | }); 71 | Object.defineProperty(savedObject, '_opSet', { 72 | enumerable: false, 73 | writable: false, 74 | configurable: false, 75 | value: {}, 76 | }); 77 | return savedObject; 78 | }); 79 | } 80 | -------------------------------------------------------------------------------- /src/Types.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | export type GeoPoint = { 4 | __type: 'GeoPoint', 5 | latitude: number, 6 | longitude: number; 7 | }; 8 | 9 | export type ParseFile = { 10 | __type: 'File', 11 | name: string, 12 | url: string, 13 | }; 14 | 15 | export type Pointer = { 16 | __type: 'Pointer', 17 | objectId: string, 18 | className: string, 19 | }; 20 | 21 | export type WireFormatDate = { 22 | __type: 'Date', 23 | iso: string, 24 | }; 25 | 26 | export type AuthOptions = { 27 | sessionToken?: string, 28 | useMasterKey?: boolean, 29 | }; 30 | 31 | export type Identifier = string | {objectId: string, [key: string]: any}; 32 | export type ByteString = string; 33 | export type Comparable = number | string | Date; 34 | export type ParseScalar = string | number | boolean | GeoPoint | ParseFile | Pointer | Date; 35 | export type ParseMap = { [key: string]: ?ParseScalar | ParseMap | ParseVector }; 36 | export type ParseVector = Array; 37 | 38 | export type ParseValue = ParseScalar | ParseMap | ParseVector; 39 | 40 | export type ParseObject = { 41 | _serverData?: ParseMap, 42 | _opSet?: OpSet, 43 | _className?: string, 44 | [key: string]: ?ParseScalar | ParseMap | ParseVector, 45 | }; 46 | 47 | export type Op = { __op: 'Unset' } 48 | | { __op: 'Increment', amount: number } 49 | | { __op: 'Add', objects: Array }; 50 | 51 | export type OpSet = { 52 | [field: string]: Op, 53 | }; 54 | -------------------------------------------------------------------------------- /src/User.js: -------------------------------------------------------------------------------- 1 | /* @flow */ 2 | 3 | import copyWithoutFields from './copyWithoutFields'; 4 | import {decode} from './WireFormat'; 5 | import * as Ops from './Ops'; 6 | import Save from './Save'; 7 | 8 | import type App from './App'; 9 | import type { ParseObject } from './Types'; 10 | 11 | type SignUpOptions = { 12 | username: string, 13 | password: string, 14 | email?: string, 15 | }; 16 | 17 | type LogInOptions = { 18 | username: string, 19 | password: string, 20 | }; 21 | 22 | type AuthResponse = { 23 | sessionToken: string, 24 | user: ParseObject, 25 | }; 26 | 27 | export const CLASS_NAME = '_User'; 28 | 29 | export function signUp(app: App, options: SignUpOptions): Promise { 30 | const user: ParseObject = { 31 | username: options.username, 32 | password: options.password, 33 | }; 34 | if (options.email) { 35 | user.email = options.email; 36 | } 37 | return Save(app, '_User', user).then((u) => { 38 | if (!u.sessionToken) { 39 | return Promise.reject('Did not receive an authenticated user from the server.'); 40 | } 41 | const sanitized = copyWithoutFields(u, ['password', 'sessionToken']); 42 | return Promise.resolve({ 43 | user: sanitized, 44 | sessionToken: u.sessionToken, 45 | }); 46 | }); 47 | } 48 | 49 | export function logIn(app: App, options: LogInOptions): Promise { 50 | return app.client.get('login', options, {}).then((res) => { 51 | const result = res.response; 52 | if (!result) { 53 | return result; 54 | } 55 | const u: ParseObject = (decode(result): any); 56 | const sanitized = copyWithoutFields(u, ['sessionToken']); 57 | const serverData = {}; 58 | for (let k in sanitized) { 59 | serverData[k] = sanitized[k]; 60 | } 61 | Object.defineProperty(sanitized, '_serverData', { 62 | enumerable: false, 63 | writeable: false, 64 | configurable: false, 65 | value: serverData, 66 | }); 67 | Object.defineProperty(sanitized, '_className', { 68 | enumerable: false, 69 | writeable: false, 70 | configurable: false, 71 | value: '_User', 72 | }); 73 | const token = typeof u.sessionToken === 'string' ? u.sessionToken : ''; 74 | return Promise.resolve({ 75 | user: sanitized, 76 | sessionToken: token, 77 | }); 78 | }); 79 | } 80 | 81 | // "become" doesn't make a lot of sense in this sort of library 82 | export function forSession(app: App, token: string): Promise { 83 | return app.client.get('users/me', {}, {sessionToken: token}).then((res) => { 84 | const result = res.response; 85 | if (!result) { 86 | return result; 87 | } 88 | const u: ParseObject = (decode(result): any); 89 | return Promise.resolve(copyWithoutFields(u, ['sessionToken'])); 90 | }); 91 | } 92 | 93 | export function logOut(app: App, token: string): Promise { 94 | return app.client.post('logout', {}, {sessionToken: token}).then(() => { 95 | return Promise.resolve(); 96 | }); 97 | } 98 | -------------------------------------------------------------------------------- /src/WireFormat.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Allows objects to be built from or encoded to the wire format used by the 3 | * Parse-Server to represent complex data types. 4 | * @flow 5 | */ 6 | 7 | import {isDate} from './Date'; 8 | 9 | import type { 10 | GeoPoint, 11 | ParseFile, 12 | ParseValue, 13 | Pointer, 14 | WireFormatDate, 15 | } from './Types'; 16 | 17 | export function encode(data: any, forcePointer?: boolean): ?ParseValue { 18 | if ( 19 | typeof data === 'undefined' || 20 | typeof data === 'boolean' || 21 | typeof data === 'string' || 22 | typeof data === 'number' 23 | ) { 24 | return data; 25 | } 26 | if (data === null) { 27 | return null; 28 | } 29 | if (Array.isArray(data)) { 30 | return data.map(encode); 31 | } 32 | if (isDate(data)) { 33 | const date: WireFormatDate = { 34 | __type: 'Date', 35 | iso: data.toJSON() 36 | }; 37 | return date; 38 | } 39 | if (data.__type) { 40 | if (data.__type === 'File') { 41 | const file: ParseFile = { 42 | __type: 'File', 43 | name: data.name, 44 | url: data.url, 45 | }; 46 | } 47 | if (data.__type === 'GeoPoint') { 48 | const point: GeoPoint = { 49 | __type: 'GeoPoint', 50 | latitude: data.latitude, 51 | longitude: data.longitude, 52 | }; 53 | } 54 | } 55 | if (data.hasOwnProperty('latitude') && 56 | data.hasOwnProperty('longitude') && 57 | Object.keys(data).length === 2) { 58 | const point: GeoPoint = { 59 | __type: 'GeoPoint', 60 | latitude: data.latitude, 61 | longitude: data.longitude, 62 | }; 63 | return point; 64 | } 65 | if (data.objectId) { 66 | if (data._className) { 67 | if (forcePointer) { 68 | const pointer: Pointer = { 69 | __type: 'Pointer', 70 | objectId: data.objectId, 71 | className: data._className 72 | }; 73 | return pointer; 74 | } 75 | } 76 | } 77 | let encoded = {}; 78 | for (let key in data) { 79 | encoded[key] = encode(data[key], true); 80 | } 81 | return encoded; 82 | } 83 | 84 | export function decode(data: any): ?ParseValue { 85 | if ( 86 | typeof data === 'undefined' || 87 | typeof data === 'boolean' || 88 | typeof data === 'string' || 89 | typeof data === 'number' 90 | ) { 91 | return data; 92 | } 93 | if (data === null) { 94 | return null; 95 | } 96 | if (Array.isArray(data)) { 97 | return data.map(decode); 98 | } 99 | if (data.__type === 'Date') { 100 | return new Date(data.iso); 101 | } 102 | const decoded = {}; 103 | if (data.__type) { 104 | Object.defineProperty(decoded, '__type', { 105 | enumerable: false, 106 | writable: false, 107 | configurable: false, 108 | value: data.__type, 109 | }); 110 | } 111 | for (let key in data) { 112 | if (key === 'createdAt' || key === 'updatedAt') { 113 | if (typeof data[key] === 'string') { 114 | const date = new Date(data[key]); 115 | if (!isNaN(date)) { 116 | decoded[key] = date; 117 | continue; 118 | } 119 | } 120 | } 121 | if (key !== '__type') { 122 | decoded[key] = decode(data[key]) 123 | } 124 | } 125 | return decoded; 126 | } 127 | -------------------------------------------------------------------------------- /src/arrayValueMatches.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Determines if two values in an Array field are equal, 3 | * used for atomic remove / addUnique ops 4 | */ 5 | 6 | export default function arrayValueMatches(a, b) { 7 | if (a === b) { 8 | return true; 9 | } 10 | if (a && b) { 11 | if (typeof a === 'object' && typeof b === 'object') { 12 | return a.objectId === b.objectId; 13 | } 14 | } 15 | return false; 16 | } 17 | -------------------------------------------------------------------------------- /src/copyWithoutFields.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Create a shallow copy of an object that excludes certain fields. 3 | * This ensures that they're "forgotten" from both the object and 4 | * its private serverData. 5 | * 6 | * @flow 7 | */ 8 | 9 | import type {ParseObject} from './Types'; 10 | 11 | export default function copyWithoutFields( 12 | object: ParseObject, 13 | fields: Array 14 | ): ParseObject { 15 | const copy = {}; 16 | for (let key in object) { 17 | if (fields.indexOf(key) < 0) { 18 | copy[key] = object[key]; 19 | } 20 | } 21 | if (object.hasOwnProperty('_serverData')) { 22 | Object.defineProperty(copy, '_serverData', { 23 | enumerable: false, 24 | writable: false, 25 | configurable: false, 26 | value: object._serverData, 27 | }); 28 | } 29 | if (object.hasOwnProperty('_className')) { 30 | Object.defineProperty(copy, '_className', { 31 | enumerable: false, 32 | writable: false, 33 | configurable: false, 34 | value: object._className, 35 | }); 36 | } 37 | if (object.hasOwnProperty('_opSet')) { 38 | Object.defineProperty(copy, '_opSet', { 39 | enumerable: false, 40 | writable: false, 41 | configurable: false, 42 | value: object._opSet, 43 | }); 44 | } 45 | 46 | return copy; 47 | } 48 | -------------------------------------------------------------------------------- /src/deepCopy.js: -------------------------------------------------------------------------------- 1 | import {isDate} from './Date'; 2 | 3 | export default function deepCopy(obj, seen = []) { 4 | if (!obj || typeof obj !== 'object') { 5 | return obj; 6 | } 7 | if (isDate(obj)) { 8 | return new Date(obj.getTime()); 9 | } 10 | if (seen.indexOf(obj) > -1) { 11 | throw new Error('Cannot copy circular object references'); 12 | } 13 | let newSeen = seen.concat([obj]); 14 | if (Array.isArray(obj)) { 15 | return obj.map(el => deepCopy(el, newSeen)); 16 | } 17 | let copy = {}; 18 | for (let k in obj) { 19 | copy[k] = deepCopy(obj[k], newSeen); 20 | } 21 | return copy; 22 | } 23 | -------------------------------------------------------------------------------- /src/defs/ibeam.js: -------------------------------------------------------------------------------- 1 | declare module 'ibeam' { 2 | declare type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE'; 3 | declare type HttpHeaders = { [header: string]: string }; 4 | 5 | declare type ClientOptions = { 6 | https?: boolean, 7 | host?: string, 8 | headers?: HttpHeaders, 9 | }; 10 | 11 | declare type PreProcessor = ( 12 | method: HttpMethod, 13 | path: string, 14 | payload: any, 15 | options: any 16 | ) => any; 17 | 18 | declare type HttpControllerResponse = { 19 | status: number, 20 | response: string, 21 | }; 22 | 23 | declare type PostProcessor = ( 24 | response: HttpControllerResponse 25 | ) => any; 26 | 27 | declare class Client { 28 | constructor(config: {[opt: string]: any}): Client; 29 | getConfig(): {[opt: string]: any}; 30 | get(path: string, payload: any, options?: any): Promise; 31 | post(path: string, payload: any, options?: any): Promise; 32 | put(path: string, payload: any, options?: any): Promise; 33 | patch(path: string, payload: any, options?: any): Promise; 34 | delete(path: string, payload: any, options?: any): Promise; 35 | raw(method: HttpMethod, path: string, payload: any, options?: any): Promise; 36 | } 37 | 38 | declare function addRequestPreProcessor( 39 | client: Client, 40 | processor: PreProcessor 41 | ): Client; 42 | 43 | declare function addResponsePostProcessor( 44 | client: Client, 45 | processor: PostProcessor 46 | ): Client; 47 | } 48 | -------------------------------------------------------------------------------- /test/Ops-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | import {expect} from 'chai'; 3 | import * as Ops from '../src/Ops'; 4 | 5 | suite('Ops', () => { 6 | test('set', () => { 7 | let result = Ops.set({ a: 5 }, { b: 'str' }); 8 | expect(Object.keys(result)).to.deep.equal(['a', 'b']); 9 | expect(result.a).to.equal(5); 10 | expect(result.b).to.equal('str'); 11 | expect(result._serverData).to.be.empty; 12 | expect(result._opSet).to.deep.equal({ a: 5, b: 'str' }); 13 | 14 | result = Ops.set(result, { c: false }); 15 | expect(result.c).to.equal(false); 16 | expect(result._serverData).to.be.empty; 17 | expect(result._opSet).to.deep.equal({ a: 5, b: 'str', c: false }); 18 | 19 | result = Ops.set(result, { b: 'str2' }); 20 | expect(result.b).to.equal('str2'); 21 | expect(result._serverData).to.be.empty; 22 | expect(result._opSet).to.deep.equal({ a: 5, b: 'str2', c: false }); 23 | }); 24 | 25 | test('set on existing objects', () => { 26 | let result = Ops.set({ objectId: 'abc' }, { b: 'str' }); 27 | expect(result.objectId).to.equal('abc'); 28 | expect(result.b).to.equal('str'); 29 | expect(result._serverData).to.be.empty; 30 | expect(result._opSet).to.deep.equal({ b: 'str' }); 31 | }); 32 | 33 | test('implicit set has no effect', () => { 34 | let obj = { objectId: 'abc', a: 4 }; 35 | Object.defineProperty(obj, '_serverData', { 36 | enumerable: false, 37 | writable: false, 38 | configurable: false, 39 | value: { a: 4 }, 40 | }); 41 | obj.a = 5; 42 | let result = Ops.set(obj, {}); 43 | expect(result.objectId).to.equal('abc'); 44 | expect(result.a).to.equal(4); 45 | expect(result._serverData).to.deep.equal({ a: 4 }); 46 | expect(result._opSet).to.be.empty; 47 | 48 | obj = { objectId: 'abc', a: 4 }; 49 | Object.defineProperty(obj, '_serverData', { 50 | enumerable: false, 51 | writable: false, 52 | configurable: false, 53 | value: { a: 4 }, 54 | }); 55 | obj.a = 5; 56 | result = Ops.set(obj, { b: 6 }); 57 | expect(result.objectId).to.equal('abc'); 58 | expect(result.a).to.equal(4); 59 | expect(result.b).to.equal(6); 60 | expect(result._serverData).to.deep.equal({ a: 4 }); 61 | expect(result._opSet).to.deep.equal({ b: 6 }); 62 | }); 63 | 64 | test('set is not vulnerable to side effects', () => { 65 | let obj = { a: 5 }; 66 | let result = Ops.set({ objectId: 'abc' }, { obj: obj }); 67 | expect(result.obj).to.deep.equal({ a: 5 }); 68 | obj.a = 12; 69 | expect(result.obj).to.deep.equal({ a: 5 }); 70 | }); 71 | 72 | test('unset', () => { 73 | let result = Ops.unset({ a: 5, b: 7 }, 'a'); 74 | expect(result).to.not.have.property('a'); 75 | expect(result._serverData).to.be.empty; 76 | expect(result._opSet).to.deep.equal({ b: 7 }); 77 | 78 | result = Ops.unset(result, 'b'); 79 | expect(result).to.not.have.property('b'); 80 | expect(result._serverData).to.be.empty; 81 | expect(result._opSet).to.be.empty; 82 | }); 83 | 84 | test('unset on existing objects', () => { 85 | let result = Ops.unset({ objectId: 'abc' }, 'a'); 86 | expect(result._opSet).to.deep.equal({ a: { __op: 'Delete' }}); 87 | 88 | let obj = { objectId: 'abc', a: 4, b: 6 }; 89 | Object.defineProperty(obj, '_serverData', { 90 | enumerable: false, 91 | writable: false, 92 | configurable: false, 93 | value: { a: 4, b: 6 }, 94 | }); 95 | result = Ops.unset(obj, 'a'); 96 | expect(result).to.not.have.property('a'); 97 | expect(result._opSet).to.deep.equal({ a: { __op: 'Delete' }}); 98 | expect(result._serverData).to.deep.equal({ a: 4, b: 6 }); 99 | }); 100 | 101 | test('increment', () => { 102 | let result = Ops.increment({ a: 5 }, 'a'); 103 | expect(result.a).to.equal(6); 104 | expect(result._serverData).to.be.empty; 105 | expect(result._opSet).to.deep.equal({ a: 6 }); 106 | 107 | result = Ops.increment(result, 'a', 10); 108 | expect(result.a).to.equal(16); 109 | expect(result._serverData).to.be.empty; 110 | expect(result._opSet).to.deep.equal({ a: 16 }); 111 | }); 112 | 113 | test('add', () => { 114 | let result = Ops.add({ tags: ['a', 'b'] }, 'tags', 'c'); 115 | expect(result.tags).to.deep.equal(['a', 'b', 'c']); 116 | 117 | result = Ops.add(result, 'tags', ['d', 'e', 'f']); 118 | expect(result.tags).to.deep.equal(['a', 'b', 'c', 'd', 'e', 'f']); 119 | }); 120 | 121 | test('remove', () => { 122 | let result = Ops.remove({ tags: ['a', 'b', 'c', 'd', 'e', 'f'] }, 'tags', 'a'); 123 | expect(result.tags).to.deep.equal(['b', 'c', 'd', 'e', 'f']); 124 | 125 | result = Ops.remove(result, 'tags', 'b'); 126 | expect(result.tags).to.deep.equal(['c', 'd', 'e', 'f']); 127 | 128 | result = Ops.remove(result, 'tags', 'f'); 129 | expect(result.tags).to.deep.equal(['c', 'd', 'e']); 130 | 131 | result = Ops.remove(result, 'tags', ['d', 'e']); 132 | expect(result.tags).to.deep.equal(['c']); 133 | 134 | result = Ops.remove(result, 'tags', ['c', 'e']); 135 | expect(result.tags).to.deep.equal([]); 136 | }); 137 | 138 | test('add unique', () => { 139 | let result = Ops.addUnique({ tags: ['a', 'b'] }, 'tags', 'c'); 140 | expect(result.tags).to.deep.equal(['a', 'b', 'c']); 141 | 142 | result = Ops.addUnique(result, 'tags', ['d', 'e', 'b']); 143 | expect(result.tags).to.deep.equal(['a', 'b', 'c', 'd', 'e']); 144 | 145 | result = Ops.addUnique(result, 'tags', ['d', 'e']); 146 | expect(result.tags).to.deep.equal(['a', 'b', 'c', 'd', 'e']); 147 | }); 148 | 149 | test('array operations are not vulnerable to side effects', () => { 150 | let a = {a: 5}; 151 | let b = {b: 10}; 152 | let original = {arr: [a]}; 153 | let result = Ops.add(original, 'arr', b); 154 | expect(result.arr[0]).to.deep.equal({a: 5}); 155 | expect(result.arr[1]).to.deep.equal({b: 10}); 156 | b.b = 20; 157 | expect(result.arr[1]).to.deep.equal({b: 10}); 158 | }); 159 | }); 160 | -------------------------------------------------------------------------------- /test/Query-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import * as Query from '../src/Query'; 5 | 6 | class App { 7 | constructor() {} 8 | } 9 | 10 | suite('Query', () => { 11 | test('equal to', () => { 12 | let q = Query.emptyQuery(); 13 | let eq = Query.equalTo(q, 'foo', 'bar'); 14 | expect(q).to.not.equal(eq); 15 | expect(q.where).to.deep.equal({}); 16 | expect(eq.where).to.deep.equal({foo: 'bar'}); 17 | 18 | eq = Query.equalTo(eq, 'foo', 'baz'); 19 | expect(eq.where).to.deep.equal({foo: 'baz'}); 20 | 21 | eq = Query.equalTo(eq, 'count', 10); 22 | expect(eq.where).to.deep.equal({foo: 'baz', count: 10}); 23 | }); 24 | 25 | test('not equal to', () => { 26 | let q = Query.emptyQuery(); 27 | let eq = Query.notEqualTo(q, 'foo', 'bar'); 28 | expect(q).to.not.equal(eq); 29 | expect(q.where).to.deep.equal({}); 30 | expect(eq.where).to.deep.equal({foo: {$ne: 'bar'}}); 31 | 32 | eq = Query.equalTo(eq, 'foo', 'bar'); 33 | expect(eq.where).to.deep.equal({foo: 'bar'}); 34 | eq = Query.notEqualTo(eq, 'foo', 'baz'); 35 | expect(eq.where).to.deep.equal({foo: {$ne: 'baz'}}); 36 | }); 37 | 38 | test('less than', () => { 39 | let q = Query.emptyQuery(); 40 | let eq = Query.lessThan(q, 'count', 10); 41 | 42 | expect(q).to.not.equal(eq); 43 | expect(q.where).to.deep.equal({}); 44 | expect(eq.where).to.deep.equal({count: {$lt: 10}}); 45 | 46 | eq = Query.notEqualTo(eq, 'count', 3); 47 | eq = Query.lessThan(eq, 'count', 5); 48 | expect(eq.where).to.deep.equal({count: {$ne: 3, $lt: 5}}); 49 | }); 50 | 51 | test('greater than', () => { 52 | let q = Query.emptyQuery(); 53 | let eq = Query.greaterThan(q, 'count', 0); 54 | expect(q).to.not.equal(eq); 55 | expect(q.where).to.deep.equal({}); 56 | expect(eq.where).to.deep.equal({count: {$gt: 0}}); 57 | 58 | eq = Query.lessThan(eq, 'count', 10); 59 | eq = Query.greaterThan(eq, 'count', 5); 60 | expect(eq.where).to.deep.equal({count: {$lt: 10, $gt: 5}}); 61 | }); 62 | 63 | test('less than or equal to', () => { 64 | let q = Query.emptyQuery(); 65 | let eq = Query.lessThanOrEqualTo(q, 'count', 10); 66 | expect(q).to.not.equal(eq); 67 | expect(q.where).to.deep.equal({}); 68 | expect(eq.where).to.deep.equal({count: {$lte: 10}}); 69 | }); 70 | 71 | test('greater than or equal to', () => { 72 | let q = Query.emptyQuery(); 73 | let eq = Query.greaterThanOrEqualTo(q, 'count', 10); 74 | expect(q).to.not.equal(eq); 75 | expect(q.where).to.deep.equal({}); 76 | expect(eq.where).to.deep.equal({count: {$gte: 10}}); 77 | }); 78 | 79 | test('contained in', () => { 80 | let q = Query.emptyQuery(); 81 | let eq = Query.containedIn(q, 'tags', 'abc'); 82 | expect(q).to.not.equal(eq); 83 | expect(q.where).to.deep.equal({}); 84 | expect(eq.where).to.deep.equal({tags: {$in: 'abc'}}); 85 | }); 86 | 87 | test('notContainedIn', () => { 88 | let q = Query.emptyQuery(); 89 | let eq = Query.notContainedIn(q, 'tags', 'abc'); 90 | expect(q).to.not.equal(eq); 91 | expect(q.where).to.deep.equal({}); 92 | expect(eq.where).to.deep.equal({tags: {$nin: 'abc'}}); 93 | }); 94 | 95 | test('contains all', () => { 96 | let q = Query.emptyQuery(); 97 | let eq = Query.containsAll(q, 'tags', ['abc', 'def']); 98 | expect(q).to.not.equal(eq); 99 | expect(q.where).to.deep.equal({}); 100 | expect(eq.where).to.deep.equal({tags: {$all: ['abc', 'def']}}); 101 | }); 102 | 103 | test('exists', () => { 104 | let q = Query.emptyQuery(); 105 | let eq = Query.exists(q, 'prop'); 106 | expect(q).to.not.equal(eq); 107 | expect(q.where).to.deep.equal({}); 108 | expect(eq.where).to.deep.equal({prop: {$exists: true}}); 109 | }); 110 | 111 | test('does not exist', () => { 112 | let q = Query.emptyQuery(); 113 | let eq = Query.doesNotExist(q, 'prop'); 114 | expect(q).to.not.equal(eq); 115 | expect(q.where).to.deep.equal({}); 116 | expect(eq.where).to.deep.equal({prop: {$exists: false}}); 117 | }); 118 | 119 | test('ascending order', () => { 120 | let q = Query.emptyQuery(); 121 | let eq = Query.exists(q, 'prop'); 122 | eq = Query.ascending(eq, 'count'); 123 | expect(q).to.not.equal(eq); 124 | expect(eq.order).to.deep.equal(['count']); 125 | expect(eq.where).to.deep.equal({prop: {$exists: true}}); 126 | 127 | eq = Query.ascending(eq, 'count', 'level'); 128 | expect(eq.order).to.deep.equal(['count', 'level']); 129 | }); 130 | 131 | test('descending order', () => { 132 | let q = Query.emptyQuery(); 133 | let eq = Query.exists(q, 'prop'); 134 | eq = Query.descending(eq, 'count'); 135 | expect(q).to.not.equal(eq); 136 | expect(eq.order).to.deep.equal(['-count']); 137 | expect(eq.where).to.deep.equal({prop: {$exists: true}}); 138 | 139 | eq = Query.descending(eq, 'count', 'level'); 140 | expect(eq.order).to.deep.equal(['-count', '-level']); 141 | }); 142 | 143 | test('skip', () => { 144 | let q = Query.emptyQuery(); 145 | let eq = Query.exists(q, 'prop'); 146 | eq = Query.skip(eq, 100); 147 | expect(q).to.not.equal(eq); 148 | expect(q.skip).to.equal(0); 149 | expect(eq.skip).to.equal(100); 150 | expect(eq.where).to.deep.equal({prop: {$exists: true}}); 151 | }); 152 | 153 | test('limit', () => { 154 | let q = Query.emptyQuery(); 155 | let eq = Query.exists(q, 'prop'); 156 | eq = Query.limit(eq, 10); 157 | expect(q).to.not.equal(eq); 158 | expect(q.limit).to.equal(-1); 159 | expect(eq.limit).to.equal(10); 160 | expect(eq.where).to.deep.equal({prop: {$exists: true}}); 161 | }); 162 | 163 | test('include', () => { 164 | let q = Query.emptyQuery(); 165 | let eq = Query.exists(q, 'prop'); 166 | eq = Query.include(eq, 'field'); 167 | expect(q).to.not.equal(eq); 168 | expect(q.include).to.deep.equal([]); 169 | expect(eq.include).to.deep.equal(['field']); 170 | expect(eq.where).to.deep.equal({prop: {$exists: true}}); 171 | }); 172 | }); 173 | -------------------------------------------------------------------------------- /test/WireFormat-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import {encode} from '../src/WireFormat'; 5 | 6 | suite('encode', () => { 7 | test('primitives', () => { 8 | expect(encode(null)).to.equal(null); 9 | expect(encode(12)).to.equal(12); 10 | expect(encode('foo')).to.equal('foo'); 11 | expect(encode(false)).to.equal(false); 12 | }); 13 | 14 | test('dates', () => { 15 | let date = encode(new Date(Date.UTC(2015, 1, 1))); 16 | expect(date.__type).to.equal('Date'); 17 | expect(date.iso).to.equal('2015-02-01T00:00:00.000Z'); 18 | }); 19 | 20 | test('geopoint', () => { 21 | let geo = encode({latitude: 20, longitude: 40}); 22 | expect(geo.latitude).to.equal(20); 23 | expect(geo.longitude).to.equal(40); 24 | expect(geo.__type).to.equal('GeoPoint'); 25 | }); 26 | }); 27 | -------------------------------------------------------------------------------- /test/arrayValueMatches-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import arrayValueMatches from '../src/arrayValueMatches'; 5 | 6 | suite('arrayValueMatches', () => { 7 | test('primitive equality', () => { 8 | expect(arrayValueMatches(0, 0)).to.be.true; 9 | expect(arrayValueMatches(0, 1)).to.be.false; 10 | expect(arrayValueMatches(0, null)).to.be.false; 11 | expect(arrayValueMatches(null, null)).to.be.true; 12 | expect(arrayValueMatches(undefined, undefined)).to.be.true; 13 | expect(arrayValueMatches(2, '2')).to.be.false; 14 | expect(arrayValueMatches('2', '2')).to.be.true; 15 | }); 16 | 17 | test('object equality', () => { 18 | let a = {objectId: 'a'}; 19 | let b = {objectId: 'b'}; 20 | let a2 = {objectId: 'a'}; 21 | expect(arrayValueMatches(a, a)).to.be.true; 22 | expect(arrayValueMatches(a, b)).to.be.false; 23 | expect(arrayValueMatches(a, a2)).to.be.true; 24 | }); 25 | }); 26 | -------------------------------------------------------------------------------- /test/deepCopy-test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | import {expect} from 'chai'; 4 | import deepCopy from '../src/deepCopy'; 5 | 6 | suite('deepCopy', () => { 7 | test('primitives', () => { 8 | expect(deepCopy(null)).to.equal(null); 9 | expect(deepCopy(12)).to.equal(12); 10 | expect(deepCopy('foo')).to.equal('foo'); 11 | expect(deepCopy(false)).to.equal(false); 12 | }); 13 | 14 | test('dates', () => { 15 | let d = new Date(); 16 | let copy = deepCopy(d); 17 | expect(d).to.not.equal(copy); 18 | expect(d.getTime()).to.equal(copy.getTime()); 19 | }); 20 | 21 | test('arrays', () => { 22 | let arr = [1,3,5]; 23 | let copy = deepCopy(arr); 24 | expect(copy).to.deep.equal([1,3,5]); 25 | expect(copy).to.not.equal(arr); 26 | 27 | arr = [1,2,[3,4,5]]; 28 | copy = deepCopy(arr); 29 | expect(copy).to.deep.equal([1,2,[3,4,5]]); 30 | expect(copy).to.not.equal(arr); 31 | expect(copy[2]).to.not.equal(arr[2]); 32 | 33 | let a = ['a', 'b']; 34 | let b = [[a]]; 35 | a[2] = b; 36 | expect(() => deepCopy(a)).to.throw(); 37 | }); 38 | 39 | test('objects', () => { 40 | let obj = {a: 1, b: 2}; 41 | let copy = deepCopy(obj); 42 | expect(copy).to.deep.equal(obj); 43 | expect(copy).to.not.equal(obj); 44 | 45 | obj = {a: 1, b: {c: 5}}; 46 | copy = deepCopy(obj); 47 | expect(copy).to.deep.equal(obj); 48 | expect(copy).to.not.equal(obj); 49 | expect(copy.b).to.not.equal(obj.b); 50 | expect(copy.b).to.not.equal(obj.b); 51 | 52 | obj = {a: 1, b: {c: {d: {e: 7}}}}; 53 | copy = deepCopy(obj); 54 | expect(copy).to.deep.equal(obj); 55 | expect(copy).to.not.equal(obj); 56 | expect(copy.b).to.not.equal(obj.b); 57 | expect(copy.b.c).to.not.equal(obj.b.c); 58 | expect(copy.b.c.d).to.not.equal(obj.b.c.d); 59 | 60 | let a = {a: 1, b: 2}; 61 | let b = {parent: a}; 62 | a.child = b; 63 | expect(() => deepCopy(a)).to.throw(); 64 | }); 65 | }); 66 | --------------------------------------------------------------------------------