├── .bowerrc ├── .editorconfig ├── .ember-cli ├── .gitignore ├── .jscsrc ├── .jshintrc ├── .npmignore ├── .travis.yml ├── .watchmanconfig ├── Brocfile.js ├── LICENSE.md ├── README.md ├── addon ├── adapters │ └── parse.js ├── initializers │ └── parse.js ├── mixins │ ├── authenticated-route-mixin.js │ └── unauthenticated-route-mixin.js ├── models │ └── parse-user.js ├── serializers │ └── parse.js ├── services │ ├── cloud.js │ └── session.js └── transforms │ ├── date.js │ ├── file.js │ └── geopoint.js ├── blueprints ├── ember-parse-core │ ├── files │ │ └── app │ │ │ ├── adapters │ │ │ └── application.js │ │ │ └── serializers │ │ │ └── application.js │ └── index.js └── ember-parse-session │ ├── files │ └── app │ │ ├── initializers │ │ └── parse.js │ │ ├── models │ │ └── user.js │ │ └── services │ │ └── parse.js │ └── index.js ├── bower.json ├── config ├── ember-try.js └── environment.js ├── index.js ├── package.json ├── testem.json ├── tests ├── .jshintrc ├── dummy │ ├── app │ │ ├── adapters │ │ │ └── application.js │ │ ├── app.js │ │ ├── components │ │ │ └── .gitkeep │ │ ├── controllers │ │ │ ├── .gitkeep │ │ │ └── application.js │ │ ├── helpers │ │ │ └── .gitkeep │ │ ├── index.html │ │ ├── initializers │ │ │ └── parse.js │ │ ├── models │ │ │ ├── .gitkeep │ │ │ ├── car.js │ │ │ ├── category.js │ │ │ ├── friend.js │ │ │ ├── thing.js │ │ │ └── user.js │ │ ├── router.js │ │ ├── routes │ │ │ └── application.js │ │ ├── serializers │ │ │ └── application.js │ │ ├── services │ │ │ └── cloud.js │ │ ├── styles │ │ │ └── app.css │ │ ├── templates │ │ │ ├── application.hbs │ │ │ └── components │ │ │ │ └── .gitkeep │ │ └── views │ │ │ └── .gitkeep │ ├── config │ │ └── environment.js │ └── public │ │ ├── crossdomain.xml │ │ └── robots.txt ├── helpers │ ├── resolver.js │ └── start-app.js ├── index.html ├── test-helper.js └── unit │ ├── .gitkeep │ └── adapters │ └── parse-test.js └── vendor └── .gitkeep /.bowerrc: -------------------------------------------------------------------------------- 1 | { 2 | "directory": "bower_components", 3 | "analytics": false 4 | } 5 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # EditorConfig helps developers define and maintain consistent 2 | # coding styles between different editors and IDEs 3 | # editorconfig.org 4 | 5 | root = true 6 | 7 | 8 | [*] 9 | end_of_line = lf 10 | charset = utf-8 11 | trim_trailing_whitespace = true 12 | insert_final_newline = true 13 | indent_style = space 14 | indent_size = 2 15 | 16 | [*.js] 17 | indent_style = space 18 | indent_size = 2 19 | 20 | [*.hbs] 21 | insert_final_newline = false 22 | indent_style = space 23 | indent_size = 2 24 | 25 | [*.css] 26 | indent_style = space 27 | indent_size = 2 28 | 29 | [*.html] 30 | indent_style = space 31 | indent_size = 2 32 | 33 | [*.{diff,md}] 34 | trim_trailing_whitespace = false 35 | -------------------------------------------------------------------------------- /.ember-cli: -------------------------------------------------------------------------------- 1 | { 2 | /** 3 | Ember CLI sends analytics information by default. The data is completely 4 | anonymous, but there are times when you might want to disable this behavior. 5 | 6 | Setting `disableAnalytics` to true will prevent any data from being sent. 7 | */ 8 | "disableAnalytics": false 9 | } 10 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /dist 5 | /tmp 6 | 7 | # dependencies 8 | /node_modules 9 | /bower_components 10 | 11 | # misc 12 | /.sass-cache 13 | /connect.lock 14 | /coverage/* 15 | /libpeerconnection.log 16 | npm-debug.log 17 | testem.log 18 | -------------------------------------------------------------------------------- /.jscsrc: -------------------------------------------------------------------------------- 1 | { 2 | "esnext": true, 3 | "disallowSpacesInsideArrayBrackets": "all", 4 | "requireSpaceBeforeObjectValues": true, 5 | "requireCommaBeforeLineBreak": true, 6 | "requireBlocksOnNewline": 1, 7 | "disallowKeywordsOnNewLine": ["else"], 8 | "disallowNewlineBeforeBlockStatements": true, 9 | "requireSpacesInConditionalExpression": { 10 | "afterTest": true, 11 | "beforeConsequent": true, 12 | "afterConsequent": true, 13 | "beforeAlternate": true 14 | }, 15 | "disallowSpacesInCallExpression": true, 16 | "disallowSpacesInsideParentheses": true, 17 | "disallowEmptyBlocks": true, 18 | "requireSpacesInsideObjectBrackets": "all", 19 | "requireCurlyBraces": [ 20 | "if", 21 | "else", 22 | "for", 23 | "while", 24 | "do", 25 | "try", 26 | "catch" 27 | ], 28 | "requireLineFeedAtFileEnd": true, 29 | "disallowTrailingWhitespace": true, 30 | "disallowTrailingComma": true, 31 | "requireSpaceBeforeBlockStatements": true, 32 | "validateIndentation": 2, 33 | "validateParameterSeparator": ", ", 34 | "requireSpaceBeforeKeywords": [ 35 | "else", 36 | "while", 37 | "catch" 38 | ], 39 | "requireSpaceAfterKeywords": [ 40 | "do", 41 | "for", 42 | "if", 43 | "else", 44 | "switch", 45 | "case", 46 | "try", 47 | "while", 48 | "with", 49 | "return" 50 | ], 51 | "requireSpaceBetweenArguments": true 52 | } 53 | -------------------------------------------------------------------------------- /.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "-Promise" 6 | ], 7 | "browser": true, 8 | "boss": true, 9 | "curly": true, 10 | "debug": false, 11 | "devel": true, 12 | "eqeqeq": true, 13 | "evil": true, 14 | "forin": false, 15 | "immed": false, 16 | "laxbreak": false, 17 | "newcap": true, 18 | "noarg": true, 19 | "noempty": false, 20 | "nonew": false, 21 | "nomen": false, 22 | "onevar": false, 23 | "plusplus": false, 24 | "regexp": false, 25 | "undef": true, 26 | "sub": true, 27 | "strict": false, 28 | "white": false, 29 | "eqnull": true, 30 | "esnext": true, 31 | "unused": true 32 | } 33 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | bower_components/ 2 | tests/ 3 | tmp/ 4 | dist/ 5 | 6 | .bowerrc 7 | .editorconfig 8 | .ember-cli 9 | .travis.yml 10 | .npmignore 11 | **/.gitkeep 12 | bower.json 13 | Brocfile.js 14 | testem.json 15 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | --- 2 | language: node_js 3 | node_js: 4 | - "0.12" 5 | 6 | sudo: false 7 | 8 | cache: 9 | directories: 10 | - node_modules 11 | 12 | env: 13 | - EMBER_TRY_SCENARIO=default 14 | - EMBER_TRY_SCENARIO=ember-release 15 | - EMBER_TRY_SCENARIO=ember-beta 16 | - EMBER_TRY_SCENARIO=ember-canary 17 | 18 | matrix: 19 | fast_finish: true 20 | allow_failures: 21 | - env: EMBER_TRY_SCENARIO=ember-canary 22 | 23 | before_install: 24 | - export PATH=/usr/local/phantomjs-2.0.0/bin:$PATH 25 | - "npm config set spin false" 26 | - "npm install -g npm@^2" 27 | 28 | install: 29 | - npm install -g bower 30 | - npm install 31 | - bower install 32 | 33 | script: 34 | - ember try $EMBER_TRY_SCENARIO test 35 | -------------------------------------------------------------------------------- /.watchmanconfig: -------------------------------------------------------------------------------- 1 | { 2 | "ignore_dirs": ["tmp"] 3 | } 4 | -------------------------------------------------------------------------------- /Brocfile.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | /* global require, module */ 3 | 4 | var EmberAddon = require('ember-cli/lib/broccoli/ember-addon'); 5 | 6 | /* 7 | This Brocfile specifes the options for the dummy test app of this 8 | addon, located in `/tests/dummy` 9 | 10 | This Brocfile does *not* influence how the addon or the app using it 11 | behave. You most likely want to be modifying `./index.js` or app's Brocfile 12 | */ 13 | 14 | var app = new EmberAddon(); 15 | 16 | module.exports = app.toTree(); 17 | -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | The MIT License (MIT) 2 | 3 | Copyright (c) 2015 Giovanni Collazo 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the "Software"), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 6 | 7 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 8 | 9 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 10 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # DEPRECATED: Work on this has moved to [ember-parse-adapter](https://github.com/clintjhill/ember-parse-adapter/). 2 | 3 | # Parse for Ember.js 4 | 5 | This addon has all you need to use [Parse](https://parse.com/) in your Ember.js application. It includes an adapter and serializer to integrate with ember-data and a session service to provide authentication. 6 | 7 | ## WORK-IN-PROGRESS 8 | This is still a work in progress. 9 | 10 | #### Tests 11 | - [ ] Test session service is injected in routes 12 | - [ ] Test session service is injected in controllers 13 | - [ ] Test session service is injected in components 14 | - [ ] Test session service can register new user 15 | - [ ] Test session service can login user 16 | - [ ] Test session service can request password reset for user 17 | - [ ] Test session service sets sessionToken in adapter 18 | - [ ] Test get single record 19 | - [ ] Test get many records 20 | - [ ] Test create record 21 | - [ ] Test update record 22 | - [ ] Test delete record 23 | - [ ] Test get belongs-to relation 24 | - [ ] Test create belongs-to relation 25 | - [ ] Test update belongs-to relation 26 | - [ ] Test delete belongs-to relation 27 | - [ ] Test get many-to-many relation 28 | - [ ] Test create many-to-many relation 29 | - [ ] Test update many-to-many relation 30 | - [ ] Test delete many-to-many relation 31 | 32 | #### Features 33 | - [ ] ApplicationRouteMixin 34 | - [X] AuthenticatedRouteMixin 35 | - [X] Blueprint to generate application files 36 | 37 | ## Getting Started 38 | Since this is still a work in progress, we don't have any documentation. In the meantime you can take a look at the [dummy app](https://github.com/GetBlimp/ember-parse/tree/master/tests/dummy) to get an idea of how the addon works. 39 | 40 | ## Installation 41 | 42 | * `ember install:addon ember-parse` 43 | * `ember generate ember-parse-core` :point_left: To add the adapter and serializer 44 | * `ember generate ember-parse-session` :point_left: To add the session service and user model 45 | 46 | #### config/environment.js 47 | 48 | ```js 49 | ENV['ember-parse'] = { 50 | PARSE_APPLICATION_ID: '', 51 | PARSE_JAVASCRIPT_KEY: '', 52 | session: { 53 | authenticationRoute: 'index', // Route where your login form is located 54 | ifAlreadyAuthenticatedRoute: 'dashboard' // Route to redirect logged in users 55 | } 56 | }; 57 | ``` 58 | 59 | ## Compatibility 60 | 61 | * ember-data >= "1.0.0-beta.19.1" 62 | 63 | ## Development 64 | 65 | * `git clone` this repository 66 | * `npm install` 67 | * `bower install` 68 | 69 | ## Running 70 | 71 | * `ember server` 72 | * Visit your app at http://localhost:4200. 73 | 74 | ## Running Tests 75 | 76 | * `ember test` 77 | * `ember test --server` 78 | 79 | ## Building 80 | 81 | * `ember build` 82 | 83 | For more information on using ember-cli, visit [http://www.ember-cli.com/](http://www.ember-cli.com/). 84 | 85 | ## Credits 86 | This addon builds upon the work of [clintjhill](https://github.com/clintjhill) on [ember-parse-adapter](https://github.com/clintjhill/ember-parse-adapter). Thanks! 87 | -------------------------------------------------------------------------------- /addon/adapters/parse.js: -------------------------------------------------------------------------------- 1 | /* 2 | * Some portions extracted from: 3 | * Parse JavaScript SDK — Version: 1.4.2 4 | * 5 | */ 6 | 7 | import Ember from 'ember'; 8 | import DS from 'ember-data'; 9 | 10 | var get = Ember.get, 11 | forEach = Ember.ArrayPolyfills.forEach; 12 | 13 | 14 | export default DS.RESTAdapter.extend({ 15 | PARSE_APPLICATION_ID: null, 16 | PARSE_JAVASCRIPT_KEY: null, 17 | PARSE_HOST: null, 18 | PARSE_NAMESPACE: null, 19 | 20 | classesPath: 'classes', 21 | parseClientVersion: 'js1.4.2', 22 | 23 | init() { 24 | this._super(); 25 | 26 | this.set('applicationId', this.get('PARSE_APPLICATION_ID')); 27 | this.set('javascriptKey', this.get('PARSE_JAVASCRIPT_KEY')); 28 | this.set('host', this.get('PARSE_HOST') || 'https://api.parse.com'); 29 | this.set('namespace', this.get('PARSE_NAMESPACE') || '1'); 30 | this.set('installationId', this._getInstallationId()); 31 | this.set('sessionToken', null); 32 | this.set('userId', null); 33 | 34 | /* 35 | * avoid pre-flight. 36 | * Parse._ajax 37 | */ 38 | this.set('headers', { 'Content-Type': 'text/plain' }); 39 | }, 40 | 41 | _getInstallationId() { 42 | /* 43 | * Parse._getInstallationId 44 | */ 45 | let lsKey = `ember-parse/${this.get('applicationId')}/installationId`; 46 | 47 | if (this.get('installationId')) { 48 | return this.get('installationId'); 49 | 50 | } else if (localStorage.getItem(lsKey)) { 51 | return localStorage.getItem(lsKey); 52 | 53 | } else { 54 | let hexOctet = function() { 55 | return ( 56 | Math.floor((1 + Math.random()) * 0x10000).toString(16).substring(1) 57 | ); 58 | }; 59 | 60 | let installationId = ( 61 | hexOctet() + hexOctet() + "-" + 62 | hexOctet() + "-" + 63 | hexOctet() + "-" + 64 | hexOctet() + "-" + 65 | hexOctet() + hexOctet() + hexOctet()); 66 | 67 | localStorage.setItem(lsKey, installationId); 68 | return installationId; 69 | } 70 | }, 71 | 72 | ajaxOptions(url, type, options) { 73 | var hash = options || {}; 74 | hash.data = hash.data || {}; 75 | hash.url = url; 76 | hash.type = type; 77 | hash.dataType = 'json'; 78 | hash.context = this; 79 | 80 | if ((hash.data && type !== 'GET')) { 81 | hash.contentType = 'application/json; charset=utf-8'; 82 | 83 | // Parse auth stuff 84 | hash.data._ClientVersion = this.get('parseClientVersion'); 85 | hash.data._ApplicationId = this.get('applicationId'); 86 | hash.data._JavaScriptKey = this.get('javascriptKey'); 87 | hash.data._InstallationId = this.get('installationId'); 88 | 89 | var _sessionToken = this.get('sessionToken'); 90 | if (_sessionToken) { 91 | hash.data._SessionToken = _sessionToken; 92 | } 93 | 94 | hash.data = JSON.stringify(hash.data); 95 | } 96 | 97 | var headers = get(this, 'headers'); 98 | if (headers !== undefined) { 99 | hash.beforeSend = function (xhr) { 100 | forEach.call(Ember.keys(headers), function(key) { 101 | xhr.setRequestHeader(key, headers[key]); 102 | }); 103 | }; 104 | } 105 | 106 | return hash; 107 | }, 108 | 109 | ajaxError(jqXHR, responseText, errorThrown) { 110 | if (jqXHR.responseJSON.error === 'invalid session token') { 111 | // invalid session 112 | var session = this.container.lookup('service:session'); 113 | session.resetSession(); 114 | } 115 | 116 | return this._super(jqXHR, responseText, errorThrown); 117 | }, 118 | 119 | normalizeErrorResponse: function(status, headers, payload) { 120 | return [ 121 | { 122 | status: `${status}`, 123 | title: 'The backend responded with an error', 124 | details: payload.error, 125 | code: payload.code 126 | } 127 | ]; 128 | }, 129 | 130 | pathForType(type) { 131 | if ('user' === type) { 132 | return 'users'; 133 | 134 | } else if ('login' === type) { 135 | return type; 136 | 137 | } else if ('logout' === type) { 138 | return type; 139 | 140 | } else if ('requestPasswordReset' === type) { 141 | return type; 142 | 143 | } else if ('functions' === type) { 144 | return 'functions'; 145 | 146 | } else { 147 | return this.classesPath + '/' + this.parsePathForType(type); 148 | } 149 | }, 150 | 151 | // Using TitleStyle is recommended by Parse 152 | parsePathForType(type) { 153 | return Ember.String.capitalize(Ember.String.camelize(type)); 154 | }, 155 | 156 | parseClassName(key) { 157 | return Ember.String.capitalize(key); 158 | }, 159 | 160 | /** 161 | * Because Parse doesn't return a full set of properties on the 162 | * responses to updates, we want to perform a merge of the response 163 | * properties onto existing data so that the record maintains 164 | * latest data. 165 | */ 166 | createRecord(store, type, record) { 167 | var serializer = store.serializerFor(type.modelName), 168 | data = { _method: 'POST' }, 169 | adapter = this; 170 | 171 | serializer.serializeIntoHash(data, type, record, { includeId: true }); 172 | 173 | var promise = new Ember.RSVP.Promise(function(resolve, reject) { 174 | adapter.ajax(adapter.buildURL(type.modelName), 'POST', { data: data }) 175 | .then(function(json) { 176 | var completed = Ember.merge(data, json); 177 | resolve(completed); 178 | }, function(reason) { 179 | var err = `Code ${reason.responseJSON.code}: ${reason.responseJSON.error}`; 180 | reject(new Error(err)); 181 | }); 182 | }); 183 | 184 | return promise; 185 | }, 186 | 187 | updateRecord(store, type, snapshot) { 188 | var data = { _method: 'PUT' }, 189 | id = snapshot.id, 190 | serializer = store.serializerFor(type.modelName); 191 | 192 | serializer.serializeIntoHash(data, type, snapshot); 193 | 194 | // debugger; 195 | // snapshot.record._relationships.friends.members 196 | // snapshot.record._relationships.friends.canonicalMembers 197 | return this.ajax(this.buildURL(type.modelName, id, snapshot), 'POST', { data: data }); 198 | }, 199 | 200 | deleteRecord(store, type, snapshot) { 201 | var data = { _method: 'DELETE' }, 202 | id = snapshot.id; 203 | 204 | return this.ajax(this.buildURL(type.modelName, id, snapshot), 'POST', { data: data }); 205 | }, 206 | 207 | findRecord(store, type, id, snapshot) { 208 | var data = { _method: 'GET' }; 209 | return this.ajax(this.buildURL(type.modelName, id, snapshot), 'POST', { data: data }); 210 | }, 211 | 212 | findAll(store, type, sinceToken) { 213 | var data = { _method: 'GET' }; 214 | 215 | if (sinceToken) { 216 | data.since = sinceToken; 217 | } 218 | 219 | data.where = {}; 220 | 221 | return this.ajax(this.buildURL(type.modelName), 'POST', { data: data }); 222 | }, 223 | 224 | /** 225 | * Implementation of a hasMany that provides a Relation query for Parse 226 | * objects. 227 | */ 228 | findHasMany(store, record, relationship) { 229 | var related = JSON.parse(relationship); 230 | 231 | var query = { 232 | where: { 233 | '$relatedTo': { 234 | 'object': { 235 | '__type': 'Pointer', 236 | 'className': this.parseClassName(record.modelName), 237 | 'objectId': record.id 238 | }, 239 | key: related.key 240 | } 241 | }, 242 | _method: 'GET' 243 | }; 244 | 245 | // the request is to the related type and not the type for the record. 246 | // the query is where there is a pointer to this record. 247 | return this.ajax( 248 | this.buildURL(related.className), 'POST', { data: query }); 249 | }, 250 | 251 | /** 252 | * Implementation of findQuery that automatically wraps query in a 253 | * JSON string. 254 | * 255 | * @example 256 | * this.store.find('comment', { 257 | * where: { 258 | * post: { 259 | * "__type": "Pointer", 260 | * "className": "Post", 261 | * "objectId": post.get('id') 262 | * } 263 | * } 264 | * }); 265 | */ 266 | findQuery(store, type, query) { 267 | query._method = 'GET'; 268 | return this.ajax(this.buildURL(type.modelName), 'POST', { data: query }); 269 | }, 270 | 271 | shouldReloadAll() { 272 | return false; 273 | }, 274 | 275 | shouldBackgroundReloadRecord() { 276 | return false; 277 | } 278 | }); 279 | -------------------------------------------------------------------------------- /addon/initializers/parse.js: -------------------------------------------------------------------------------- 1 | import ParseSession from '../services/session'; 2 | 3 | export function initialize(container) { 4 | container.register('service:session', ParseSession); 5 | container.injection('route', 'session', 'service:session'); 6 | container.injection('controller', 'session', 'service:session'); 7 | container.injection('component', 'session', 'service:session'); 8 | } 9 | 10 | export default { 11 | before: 'store', 12 | name: 'parse', 13 | initialize: initialize 14 | }; 15 | -------------------------------------------------------------------------------- /addon/mixins/authenticated-route-mixin.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | beforeModel(transition) { 5 | var superResult = this._super(transition); 6 | 7 | if (!this.get('session.isAuthenticated')) { 8 | transition.abort(); 9 | var config = this.container.lookupFactory('config:environment'); 10 | this.transitionTo(config['ember-parse'].session.authenticationRoute); 11 | } 12 | 13 | return superResult; 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /addon/mixins/unauthenticated-route-mixin.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Mixin.create({ 4 | beforeModel(transition) { 5 | var superResult = this._super(transition); 6 | 7 | if (this.get('session.isAuthenticated')) { 8 | transition.abort(); 9 | var config = this.container.lookupFactory('config:environment'); 10 | this.transitionTo(config['ember-parse'].session.ifAlreadyAuthenticatedRoute); 11 | } 12 | 13 | return superResult; 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /addon/models/parse-user.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | var attr = DS.attr; 4 | 5 | export default DS.Model.extend({ 6 | username: attr('string'), 7 | password: attr('password'), 8 | email: attr('string'), 9 | emailVerified: attr('boolean'), 10 | sessionToken: attr('string'), 11 | createdAt: attr('date'), 12 | updatedAt: attr('date') 13 | }); 14 | -------------------------------------------------------------------------------- /addon/serializers/parse.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import DS from 'ember-data'; 3 | 4 | export default DS.RESTSerializer.extend({ 5 | 6 | primaryKey: 'objectId', 7 | 8 | extractArray(store, primaryType, payload) { 9 | var namespacedPayload = {}; 10 | namespacedPayload[Ember.String.pluralize(primaryType.modelName)] = payload.results; 11 | 12 | return this._super(store, primaryType, namespacedPayload); 13 | }, 14 | 15 | extractSingle(store, primaryType, payload, recordId) { 16 | var namespacedPayload = {}; 17 | namespacedPayload[primaryType.modelName] = payload; // this.normalize(primaryType, payload); 18 | 19 | return this._super(store, primaryType, namespacedPayload, recordId); 20 | }, 21 | 22 | typeForRoot(key) { 23 | return Ember.String.dasherize(Ember.String.singularize(key)); 24 | }, 25 | 26 | /** 27 | * Because Parse only returns the updatedAt/createdAt values on updates 28 | * we have to intercept it here to assure that the adapter knows which 29 | * record ID we are dealing with (using the primaryKey). 30 | */ 31 | extract(store, type, payload, id, requestType) { 32 | if (id !== null && ('updateRecord' === requestType || 'deleteRecord' === requestType)) { 33 | payload[this.get('primaryKey')] = id; 34 | } 35 | 36 | return this._super(store, type, payload, id, requestType); 37 | }, 38 | 39 | /** 40 | * Extracts count from the payload so that you can get the total number 41 | * of records in Parse if you're using skip and limit. 42 | */ 43 | extractMeta(store, type, payload) { 44 | if (payload && payload.count) { 45 | store.metaForType(type, { count: payload.count }); 46 | delete payload.count; 47 | } 48 | }, 49 | 50 | /** 51 | * Special handling for the Date objects inside the properties of 52 | * Parse responses. 53 | */ 54 | normalizeAttributes(type, hash) { 55 | type.eachAttribute(function(key, meta) { 56 | if ('date' === meta.type && 'object' === Ember.typeOf(hash[key]) && hash[key].iso) { 57 | hash[key] = hash[key].iso; //new Date(hash[key].iso).toISOString(); 58 | } 59 | }); 60 | 61 | this._super(type, hash); 62 | }, 63 | 64 | normalizeRelationships(type, hash) { 65 | if (this.keyForRelationship) { 66 | type.eachRelationship(function(key, relationship) { 67 | if (hash[key] && 'belongsTo' === relationship.kind) { 68 | hash[key] = hash[key].objectId; 69 | } 70 | 71 | /* 72 | * TODO: Find a better way to do this 73 | * Here we set the links property to a serialized version 74 | * of key and className. This info will be passed to 75 | * adapter.findHasMany where we can deserialize to create 76 | * the needed Parse query. 77 | */ 78 | if (hash[key] && 'hasMany' === relationship.kind) { 79 | if (!hash[key].__op && hash[key].__op !== 'AddRelation') { 80 | if (!hash.links) { 81 | hash.links = {}; 82 | } 83 | hash.links[key] = JSON.stringify({ 84 | key: key, 85 | className: hash[key].className 86 | }); 87 | } 88 | 89 | delete hash[key].__type; 90 | delete hash[key].className; 91 | hash[key] = []; 92 | } 93 | }, this); 94 | } 95 | }, 96 | 97 | serializeAttribute(record, json, key, attribute) { 98 | // These are Parse reserved properties and we won't send them. 99 | if ('createdAt' === key || 100 | 'updatedAt' === key || 101 | 'emailVerified' === key || 102 | 'sessionToken' === key || 103 | 'password' === key 104 | ) { 105 | delete json[key]; 106 | 107 | } else { 108 | this._super(record, json, key, attribute); 109 | } 110 | }, 111 | 112 | serializeBelongsTo(record, json, relationship) { 113 | var key = relationship.key, 114 | belongsTo = record.belongsTo(key); 115 | 116 | if (belongsTo) { 117 | // @TODO: Perhaps this is working around a bug in Ember-Data? Why should 118 | // promises be returned here. 119 | if (belongsTo instanceof DS.PromiseObject) { 120 | if (!belongsTo.get('isFulfilled')) { 121 | throw new Error('belongsTo values *must* be fulfilled before attempting to serialize them'); 122 | } 123 | 124 | belongsTo = belongsTo.get('content'); 125 | } 126 | 127 | var _className = this.parseClassName(belongsTo.type.modelName); 128 | 129 | if (_className === 'User') { 130 | _className = '_User'; 131 | } 132 | 133 | json[key] = { 134 | '__type': 'Pointer', 135 | 'className': _className, 136 | 'objectId': belongsTo.id 137 | }; 138 | 139 | } 140 | }, 141 | 142 | serializeIntoHash(hash, type, snapshot, options) { 143 | var ParseACL = snapshot.record.get('ParseACL'); 144 | 145 | // Add ACL 146 | if (ParseACL) { 147 | var policy = {}; 148 | 149 | if (ParseACL.owner) { 150 | policy[ParseACL.owner] = {}; 151 | } 152 | 153 | if (ParseACL.permissions) { 154 | policy[ParseACL.owner] = ParseACL.permissions; 155 | } else { 156 | policy[ParseACL.owner] = { 157 | read: true, 158 | write: true 159 | }; 160 | } 161 | hash.ACL = policy; 162 | } 163 | 164 | Ember.merge(hash, this.serialize(snapshot, options)); 165 | }, 166 | 167 | parseClassName(key) { 168 | if ('User' === key) { 169 | return '_User'; 170 | 171 | } else { 172 | return Ember.String.capitalize(Ember.String.camelize(key)); 173 | } 174 | }, 175 | 176 | serializeHasMany(snapshot, json, relationship) { 177 | var key = relationship.key; 178 | 179 | if (this._canSerialize(key)) { 180 | var payloadKey; 181 | 182 | json[key] = { 'objects': [] }; 183 | 184 | // if provided, use the mapping provided by `attrs` in 185 | // the serializer 186 | payloadKey = this._getMappedKey(key); 187 | if (payloadKey === key && this.keyForRelationship) { 188 | payloadKey = this.keyForRelationship(key, "hasMany"); 189 | } 190 | 191 | var relationshipType = snapshot.type.determineRelationshipType(relationship); 192 | 193 | if (relationshipType === 'manyToNone' || relationshipType === 'manyToMany' || relationshipType === 'manyToOne') { 194 | var objects = [], 195 | objectsForKey = snapshot.hasMany(key), 196 | objectsBeforeUpdate = snapshot.record._internalModel._relationships.get(key).canonicalMembers, 197 | operation = 'AddRelation'; 198 | 199 | // Check if this is removing the last relation for a key 200 | if (objectsForKey.length === 0 && objectsBeforeUpdate.size === 1) { 201 | // Removing the last relation 202 | operation = 'RemoveRelation'; 203 | 204 | objects.push({ 205 | __type: 'Pointer', 206 | className: this.parseClassName(snapshot.type.typeForRelationship(key).modelName), 207 | objectId: objectsBeforeUpdate.list[0].id 208 | }); 209 | } 210 | 211 | // Determine if we are adding or removing a relationship 212 | objectsForKey.forEach((item) => { 213 | if (objectsForKey.length < objectsBeforeUpdate.size) { 214 | // Remove existing relation 215 | // 216 | // Parse needs an array of the objects we want 217 | // to remove so we have to invert the object list 218 | // to contain items to remove 219 | operation = 'RemoveRelation'; 220 | 221 | var objectsToKeepIds = objectsForKey.map(function(obj) { 222 | return obj.id; 223 | }); 224 | 225 | objectsBeforeUpdate.list.forEach((obj) => { 226 | if (objectsToKeepIds.indexOf(obj.id) < 0) { 227 | objects.push({ 228 | __type: 'Pointer', 229 | className: this.parseClassName(item.modelName), 230 | objectId: obj.id 231 | }); 232 | } 233 | }); 234 | 235 | } else { 236 | // Add a new relation 237 | // (objectsForKey.length > objectsBeforeUpdate.size) 238 | objects.push({ 239 | __type: 'Pointer', 240 | className: this.parseClassName(item.modelName), 241 | objectId: item.id 242 | }); 243 | } 244 | 245 | }); 246 | 247 | json[payloadKey] = { 248 | __op: operation, 249 | objects: objects, 250 | className: this.parseClassName(snapshot.type.modelName) 251 | }; 252 | // TODO support for polymorphic manyToNone and manyToMany relationships 253 | } 254 | } 255 | } 256 | 257 | }); 258 | -------------------------------------------------------------------------------- /addon/services/cloud.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | /* 5 | * Makes a call to a cloud function. 6 | * @param {String} name The function name. 7 | * @param {Object} data The parameters to send to the cloud function. 8 | */ 9 | run(name, data) { 10 | var store = this.container.lookup('service:store'), 11 | adapter = store.adapterFor('application'); 12 | 13 | return adapter.ajax(adapter.buildURL('functions', name), 'POST', { data: data }); 14 | } 15 | }); 16 | -------------------------------------------------------------------------------- /addon/services/session.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Service.extend({ 4 | user: null, 5 | userId: null, 6 | sessionToken: null, 7 | sessionStoreKey: Ember.computed(function() { 8 | var store = this.container.lookup('service:store'), 9 | adapter = store.adapterFor('application'); 10 | 11 | return `ember-parse/${adapter.get('applicationId')}/session`; 12 | }), 13 | 14 | init() { 15 | Ember.Logger.debug('DEBUG: Parse session service: init()'); 16 | 17 | var key = this.get('sessionStoreKey'), 18 | store = this.container.lookup('service:store'), 19 | model = store.modelFor('user'), 20 | adapter = store.adapterFor('application'), 21 | serializer = store.serializerFor('user'); 22 | 23 | this.sessionStore.get(key).then((sessionData) => { 24 | if (sessionData && sessionData.userId && 25 | sessionData.sessionToken && sessionData._response) { 26 | 27 | this.setProperties({ 28 | userId: sessionData.userId, 29 | sessionToken: sessionData.sessionToken 30 | }); 31 | 32 | // Create a user instance and push to store 33 | serializer.normalize(model, sessionData._response); 34 | var record = store.push('user', sessionData._response); 35 | this.user = record; 36 | 37 | // Set adapter properties 38 | delete sessionData._response; 39 | adapter.setProperties(sessionData); 40 | } 41 | }); 42 | }, 43 | 44 | isAuthenticated: Ember.computed('userId', 'sessionToken', function() { 45 | if (this.get('userId') || this.get('sessionToken')) { 46 | return true; 47 | } else { 48 | return false; 49 | } 50 | }), 51 | 52 | authenticate(username, password) { 53 | var key = this.get('sessionStoreKey'), 54 | store = this.container.lookup('service:store'), 55 | model = store.modelFor('user'), 56 | adapter = store.adapterFor('application'), 57 | serializer = store.serializerFor('user'); 58 | 59 | var data = { 60 | _method: 'GET', 61 | username: username, 62 | password: password 63 | }; 64 | 65 | return adapter.ajax(adapter.buildURL('login'), 'POST', { data: data }) 66 | .then((response) => { 67 | var sessionData = { 68 | userId: response.objectId, 69 | sessionToken: response.sessionToken, 70 | _response: response 71 | }; 72 | 73 | this.setProperties(sessionData); 74 | this.sessionStore.save(key, sessionData); 75 | 76 | // Set adapter properties 77 | delete sessionData._response; 78 | adapter.setProperties(sessionData); 79 | 80 | serializer.normalize(model, response); 81 | var record = store.push('user', response); 82 | this.user = record; 83 | 84 | return record; 85 | }); 86 | }, 87 | 88 | invalidate() { 89 | if (this.get('isAuthenticated')) { 90 | var store = this.container.lookup('service:store'), 91 | adapter = store.adapterFor('application'); 92 | 93 | // Call logout on Parse 94 | return adapter.ajax(adapter.buildURL('logout'), 'POST') 95 | .then(() => { 96 | return this.resetSession(); 97 | }); 98 | } else { 99 | return Ember.RSVP.resolve(); 100 | } 101 | }, 102 | 103 | resetSession() { 104 | var key = this.get('sessionStoreKey'), 105 | store = this.container.lookup('service:store'), 106 | adapter = store.adapterFor('application'); 107 | 108 | // Remove user from store 109 | store.find('user', this.get('userId')).then((user) => { 110 | user.unloadRecord(); 111 | }); 112 | 113 | var sessionData = { 114 | userId: null, 115 | sessionToken: null 116 | }; 117 | 118 | this.setProperties(sessionData); 119 | adapter.setProperties(sessionData); 120 | 121 | return this.sessionStore.destroy(key); 122 | }, 123 | 124 | signup(userData) { 125 | var store = this.container.lookup('service:store'), 126 | model = store.modelFor('user'), 127 | adapter = store.adapterFor('user'), 128 | serializer = store.serializerFor('user'); 129 | 130 | return adapter.ajax(adapter.buildURL(model.modelName), 'POST', { data: userData }) 131 | .then(function(response) { 132 | serializer.normalize(model, response); 133 | response.email = response.email || userData.email; 134 | response.username = response.username || userData.username; 135 | 136 | return store.push('user', response); 137 | }); 138 | }, 139 | 140 | requestPasswordReset(email) { 141 | var store = this.container.lookup('service:store'), 142 | adapter = store.adapterFor('application'), 143 | data = { 144 | _method: 'POST', 145 | email: email 146 | }; 147 | 148 | return adapter.ajax(adapter.buildURL('requestPasswordReset'), 'POST', { data: data }); 149 | }, 150 | 151 | sessionStore: { 152 | save(key, data) { 153 | return new Ember.RSVP.Promise(function(resolve) { 154 | resolve(localStorage.setItem(key, JSON.stringify(data))); 155 | }); 156 | }, 157 | 158 | get(key) { 159 | return new Ember.RSVP.Promise(function(resolve) { 160 | resolve(JSON.parse(localStorage.getItem(key))); 161 | }); 162 | }, 163 | 164 | destroy(key) { 165 | return new Ember.RSVP.Promise(function(resolve) { 166 | resolve(localStorage.removeItem(key)); 167 | }); 168 | } 169 | } 170 | }); 171 | -------------------------------------------------------------------------------- /addon/transforms/date.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | /* 4 | * The date transform handles Parse's custom data format. For 5 | * example a Parse date might come back from the REST API 6 | * looking like this: 7 | * 8 | * "registeredAt": { 9 | * "__type": "Date", 10 | * "iso": "2014-06-05T12:43:50.716Z" 11 | * } 12 | * 13 | * This helper deserializes that structure into a normal 14 | * JavaScript date object. In also performs the inverse: 15 | * converting a date object back into Parse's custom format. 16 | * 17 | * @class DS.Transforms.Data 18 | */ 19 | export default DS.Transform.extend({ 20 | 21 | deserialize(serialized) { 22 | if (!serialized) { 23 | return null; 24 | } 25 | 26 | return new Date(serialized); 27 | }, 28 | 29 | serialize(deserialized) { 30 | if (!deserialized) { 31 | return null; 32 | } 33 | 34 | return { 35 | __type: 'Date', 36 | iso: deserialized.toISOString() 37 | }; 38 | } 39 | 40 | }); 41 | -------------------------------------------------------------------------------- /addon/transforms/file.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import File from './file'; 3 | 4 | /* 5 | * The file transform handles Parse's custom data format. For 6 | * example a Parse file might come back from the REST API 7 | * looking like this: 8 | * 9 | * "registeredAt": { 10 | * "__type": "File", 11 | * "name": "foo.jpg", 12 | * "url": "http://some.s3.url.com/foo.jpg" 13 | * } 14 | * 15 | * This helper deserializes that structure into a special 16 | * File object. This object should not be changed, 17 | * instead set a new file object to the property. 18 | * 19 | * this.store.find('model').then(function(model){ 20 | * model.get('someFile'); // -> File object 21 | * model.get('someFile.url'); // -> someFile URL 22 | * 23 | * var file = new File('foo.jpg', url); 24 | * model.set('someFile', file); 25 | * }); 26 | * 27 | * When saving a record, the File object is likewise 28 | * serialized into the Parse REST API format. 29 | * 30 | * @class DS.Transforms.File 31 | */ 32 | export default DS.Transform.extend({ 33 | 34 | deserialize(serialized) { 35 | if (!serialized) { 36 | return null; 37 | } 38 | 39 | return File.create({ 40 | name: serialized.name, 41 | url: serialized.url 42 | }); 43 | }, 44 | 45 | serialize(deserialized) { 46 | if (!deserialized) { 47 | return null; 48 | } 49 | 50 | return { 51 | __type: 'File', 52 | name: deserialized.get('name'), 53 | url: deserialized.get('url') 54 | }; 55 | } 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /addon/transforms/geopoint.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | import GeoPoint from './geopoint'; 3 | 4 | /* 5 | * The file transform handles Parse's custom GeoPoint format. For 6 | * example a Parse file might come back from the REST API 7 | * looking like this: 8 | * 9 | * "registeredAt": { 10 | * "__type": "GeoPoint", 11 | * "latitude": 45.2934237432, 12 | * "longitude": -17.233242432 13 | * } 14 | * 15 | * This helper deserializes that structure into a special 16 | * GeoPoint object. This object should not be changed, 17 | * instead set a new file object to the property. 18 | * 19 | * this.store.find('model').then(function(model){ 20 | * model.get('someGeo'); // -> GeoPoint object 21 | * model.get('someGeo.latitude'); // -> someGeo latitude 22 | * 23 | * var geoPoint = new GeoPoint(lat, lon); 24 | * model.set('someGeo', geoPoint); 25 | * }); 26 | * 27 | * When saving a record, the GeoPoint object 28 | * is likewise serialized into the Parse REST API format. 29 | * 30 | * @class DS.Transforms.GeoPoint 31 | */ 32 | export default DS.Transform.extend({ 33 | 34 | deserialize(serialized) { 35 | if (!serialized) { 36 | return null; 37 | } 38 | 39 | return GeoPoint.create({ 40 | latitude: serialized.latitude, 41 | longitude: serialized.longitude 42 | }); 43 | }, 44 | 45 | serialize(deserialized) { 46 | if (!deserialized) { 47 | return null; 48 | } 49 | 50 | return { 51 | __type: 'GeoPoint', 52 | latitude: deserialized.get('latitude'), 53 | longitude: deserialized.get('longitude') 54 | }; 55 | } 56 | 57 | }); 58 | -------------------------------------------------------------------------------- /blueprints/ember-parse-core/files/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import ParseAdapter from 'ember-parse/adapters/parse'; 2 | import ENV from '../config/environment'; 3 | 4 | export default ParseAdapter.extend({ 5 | PARSE_APPLICATION_ID: ENV['ember-parse'].PARSE_APPLICATION_ID, 6 | PARSE_JAVASCRIPT_KEY: ENV['ember-parse'].PARSE_JAVASCRIPT_KEY, 7 | }); 8 | -------------------------------------------------------------------------------- /blueprints/ember-parse-core/files/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import ParseSerializer from 'ember-parse/serializers/parse'; 2 | 3 | export default ParseSerializer.extend({}); 4 | -------------------------------------------------------------------------------- /blueprints/ember-parse-core/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'ember-parse-core', 3 | description: 'Generates an adapter and serializer to use Parse.', 4 | 5 | // Allows the generator to not require an entity name 6 | normalizeEntityName: function(entityName) { 7 | return entityName; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /blueprints/ember-parse-session/files/app/initializers/parse.js: -------------------------------------------------------------------------------- 1 | import ParseInitializer from 'ember-parse/initializers/parse'; 2 | 3 | export default ParseInitializer; 4 | -------------------------------------------------------------------------------- /blueprints/ember-parse-session/files/app/models/user.js: -------------------------------------------------------------------------------- 1 | import ParseUser from 'ember-parse/models/parse-user'; 2 | import DS from 'ember-data'; 3 | 4 | var attr = DS.attr; 5 | 6 | export default ParseUser.extend({ 7 | /** 8 | * This model already has this attributes. 9 | * 10 | * username: attr('string'), 11 | * password: attr('password'), 12 | * email: attr('string'), 13 | * emailVerified: attr('boolean'), 14 | * sessionToken: attr('string'), 15 | * createdAt: attr('date'), 16 | * updatedAt: attr('date'), 17 | * 18 | * 19 | * Add custom attributes below. 20 | * For example: 21 | * 22 | * firstName: attr('string'), 23 | * lastName: attr('string') 24 | */ 25 | }); 26 | -------------------------------------------------------------------------------- /blueprints/ember-parse-session/files/app/services/parse.js: -------------------------------------------------------------------------------- 1 | import ParseSession from 'ember-parse/services/session'; 2 | 3 | export default ParseSession.extend({ 4 | 5 | }); 6 | -------------------------------------------------------------------------------- /blueprints/ember-parse-session/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | name: 'ember-parse-session', 3 | description: 'Generates needed files to access the Parse Session service on routes, controllers and components. It also generates a user model.', 4 | 5 | // Allows the generator to not require an entity name 6 | normalizeEntityName: function(entityName) { 7 | return entityName; 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /bower.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-parse", 3 | "dependencies": { 4 | "ember": "1.13.2", 5 | "ember-cli-shims": "ember-cli/ember-cli-shims#0.0.3", 6 | "ember-cli-test-loader": "ember-cli-test-loader#0.1.3", 7 | "ember-data": "1.13.2", 8 | "ember-load-initializers": "ember-cli/ember-load-initializers#0.1.4", 9 | "ember-qunit": "0.3.3", 10 | "ember-qunit-notifications": "0.0.7", 11 | "ember-resolver": "~0.1.15", 12 | "jquery": "^1.11.1", 13 | "loader.js": "ember-cli/loader.js#3.2.0", 14 | "qunit": "~1.17.1" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /config/ember-try.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | scenarios: [ 3 | { 4 | name: 'default', 5 | dependencies: { } 6 | }, 7 | { 8 | name: 'ember-release', 9 | dependencies: { 10 | 'ember': 'components/ember#release' 11 | }, 12 | resolutions: { 13 | 'ember': 'release' 14 | } 15 | }, 16 | { 17 | name: 'ember-beta', 18 | dependencies: { 19 | 'ember': 'components/ember#beta' 20 | }, 21 | resolutions: { 22 | 'ember': 'beta' 23 | } 24 | }, 25 | { 26 | name: 'ember-canary', 27 | dependencies: { 28 | 'ember': 'components/ember#canary' 29 | }, 30 | resolutions: { 31 | 'ember': 'canary' 32 | } 33 | } 34 | ] 35 | }; 36 | -------------------------------------------------------------------------------- /config/environment.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = function(/* environment, appConfig */) { 4 | return { }; 5 | }; 6 | -------------------------------------------------------------------------------- /index.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 'use strict'; 3 | 4 | module.exports = { 5 | name: 'ember-parse' 6 | }; 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "ember-parse", 3 | "version": "0.0.12-alpha", 4 | "description": "All you need to use Parse. Includes an adapter, serializer and a session service for auth.", 5 | "directories": { 6 | "doc": "doc", 7 | "test": "tests" 8 | }, 9 | "scripts": { 10 | "start": "ember server", 11 | "build": "ember build", 12 | "test": "ember try:testall", 13 | "clean": "rm -rf node_modules && rm -rf bower_components && rm -rf tmp && rm -rf dist" 14 | }, 15 | "repository": "https://github.com/getblimp/ember-parse", 16 | "engines": { 17 | "node": ">= 0.10.0" 18 | }, 19 | "author": "", 20 | "license": "MIT", 21 | "devDependencies": { 22 | "broccoli-asset-rev": "^2.0.2", 23 | "ember-cli": "0.2.7", 24 | "ember-cli-app-version": "0.3.3", 25 | "ember-cli-content-security-policy": "0.4.0", 26 | "ember-cli-dependency-checker": "^1.0.0", 27 | "ember-cli-github-pages": "0.0.6", 28 | "ember-cli-htmlbars": "0.7.6", 29 | "ember-cli-ic-ajax": "0.1.1", 30 | "ember-cli-inject-live-reload": "^1.3.0", 31 | "ember-cli-qunit": "0.3.13", 32 | "ember-cli-uglify": "^1.0.1", 33 | "ember-data": "1.13.2", 34 | "ember-disable-prototype-extensions": "^1.0.0", 35 | "ember-disable-proxy-controllers": "^1.0.0", 36 | "ember-export-application-global": "^1.0.2", 37 | "ember-try": "0.0.6" 38 | }, 39 | "keywords": [ 40 | "ember-addon", 41 | "ember-data", 42 | "parse" 43 | ], 44 | "dependencies": { 45 | "ember-cli-babel": "^5.0.0" 46 | }, 47 | "ember-addon": { 48 | "configPath": "tests/dummy/config", 49 | "demoURL": "https://getblimp.github.io/ember-parse/" 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /testem.json: -------------------------------------------------------------------------------- 1 | { 2 | "framework": "qunit", 3 | "test_page": "tests/index.html?hidepassed", 4 | "disable_watching": true, 5 | "launch_in_ci": [ 6 | "PhantomJS" 7 | ], 8 | "launch_in_dev": [ 9 | "PhantomJS", 10 | "Chrome" 11 | ] 12 | } 13 | -------------------------------------------------------------------------------- /tests/.jshintrc: -------------------------------------------------------------------------------- 1 | { 2 | "predef": [ 3 | "document", 4 | "window", 5 | "location", 6 | "setTimeout", 7 | "$", 8 | "-Promise", 9 | "define", 10 | "console", 11 | "visit", 12 | "exists", 13 | "fillIn", 14 | "click", 15 | "keyEvent", 16 | "triggerEvent", 17 | "find", 18 | "findWithAssert", 19 | "wait", 20 | "DS", 21 | "andThen", 22 | "currentURL", 23 | "currentPath", 24 | "currentRouteName" 25 | ], 26 | "node": false, 27 | "browser": false, 28 | "boss": true, 29 | "curly": false, 30 | "debug": false, 31 | "devel": false, 32 | "eqeqeq": true, 33 | "evil": true, 34 | "forin": false, 35 | "immed": false, 36 | "laxbreak": false, 37 | "newcap": true, 38 | "noarg": true, 39 | "noempty": false, 40 | "nonew": false, 41 | "nomen": false, 42 | "onevar": false, 43 | "plusplus": false, 44 | "regexp": false, 45 | "undef": true, 46 | "sub": true, 47 | "strict": false, 48 | "white": false, 49 | "eqnull": true, 50 | "esnext": true 51 | } 52 | -------------------------------------------------------------------------------- /tests/dummy/app/adapters/application.js: -------------------------------------------------------------------------------- 1 | import ParseAdapter from 'ember-parse/adapters/parse'; 2 | import ENV from '../config/environment'; 3 | 4 | export default ParseAdapter.extend({ 5 | PARSE_APPLICATION_ID: ENV['ember-parse'].PARSE_APPLICATION_ID, 6 | PARSE_JAVASCRIPT_KEY: ENV['ember-parse'].PARSE_JAVASCRIPT_KEY, 7 | PARSE_HOST: ENV['ember-parse'].PARSE_HOST, 8 | PARSE_NAMESPACE: ENV['ember-parse'].PARSE_NAMESPACE 9 | }); 10 | -------------------------------------------------------------------------------- /tests/dummy/app/app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Resolver from 'ember/resolver'; 3 | import loadInitializers from 'ember/load-initializers'; 4 | import config from './config/environment'; 5 | 6 | var App; 7 | 8 | Ember.MODEL_FACTORY_INJECTIONS = true; 9 | 10 | App = Ember.Application.extend({ 11 | modulePrefix: config.modulePrefix, 12 | podModulePrefix: config.podModulePrefix, 13 | Resolver: Resolver 14 | }); 15 | 16 | loadInitializers(App, config.modulePrefix); 17 | 18 | export default App; 19 | -------------------------------------------------------------------------------- /tests/dummy/app/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/dummy/app/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/dummy/app/controllers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/controllers/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Controller.extend({ 4 | cloud: Ember.inject.service('cloud'), 5 | isAuthenticated: Ember.computed.alias('session.isAuthenticated'), 6 | username: 'user@example.com', 7 | password: 'abc123', 8 | authError: null, 9 | cloudCodeResult: null, 10 | triggeredError: null, 11 | 12 | actions: { 13 | createObject() { 14 | var friend1 = this.store.createRecord('friend', { 15 | name: 'Juanito' 16 | }); 17 | 18 | var friend2 = this.store.createRecord('friend', { 19 | name: 'Paco' 20 | }); 21 | 22 | var car1 = this.store.createRecord('car', { 23 | name: 'Toyota' 24 | }); 25 | 26 | var car2 = this.store.createRecord('car', { 27 | name: 'Honda' 28 | }); 29 | 30 | var cat = this.store.createRecord('category', { 31 | name: 'Category' 32 | }); 33 | 34 | var thingObj = { 35 | name: 'New', 36 | age: 2 37 | }; 38 | 39 | if (this.get('session.userId')) { 40 | thingObj.ParseACL = { 41 | owner: this.get('session.userId') 42 | }; 43 | } 44 | 45 | var thing = this.store.createRecord('thing', thingObj); 46 | 47 | friend1.save() 48 | .then(() => friend2.save()) 49 | .then(() => car1.save()) 50 | .then(() => car2.save()) 51 | .then(() => cat.save()) 52 | .then(() => { 53 | thing.get('friends').pushObjects([friend1, friend2]); 54 | thing.get('cars').pushObjects([car1]); 55 | thing.set('category', cat); 56 | thing.save(); 57 | }); 58 | }, 59 | 60 | removeFriend(thing, friend) { 61 | thing.get('friends').removeObject(friend); 62 | thing.save(); 63 | }, 64 | 65 | deleteObject(object) { 66 | object.deleteRecord(); 67 | object.save(); 68 | }, 69 | 70 | updateObject(object) { 71 | object.set('name', 'Updated'); 72 | object.save(); 73 | }, 74 | 75 | updateCar(car) { 76 | car.set('name', car.get('name') + '*'); 77 | car.save(); 78 | }, 79 | 80 | login() { 81 | this.get('session').authenticate(this.get('username'), this.get('password')) 82 | .then((user) => { 83 | console.log('Logged in:', user.get('email')); 84 | this.set('authError', null); 85 | this.send('reloadData'); 86 | }) 87 | .catch((reason) => { 88 | var err = `Code ${reason.errors[0].code}: ${reason.errors[0].details}`; 89 | console.error(err); 90 | this.set('authError', err); 91 | }); 92 | }, 93 | 94 | logout() { 95 | this.get('session').invalidate().then(() => { 96 | console.log('Logged out'); 97 | this.send('reloadData'); 98 | }); 99 | }, 100 | 101 | signup() { 102 | this.get('session').signup({ 103 | username: this.get('username'), 104 | password: this.get('password'), 105 | email: this.get('username') 106 | }).then((user) => { 107 | console.log(user); 108 | this.send('login'); 109 | }).catch((reason) => { 110 | var err = `Code ${reason.errors[0].code}: ${reason.errors[0].details}`; 111 | console.error(err); 112 | this.set('authError', err); 113 | }); 114 | }, 115 | 116 | resetPassword() { 117 | this.get('session').requestPasswordReset(this.get('username')) 118 | .then(function(response) { 119 | console.log(response); 120 | }); 121 | }, 122 | 123 | runCloudCode() { 124 | this.get('cloud').run('sendData', { 125 | thing: 'car', 126 | color: 'red' 127 | }) 128 | .then((response) => { 129 | response.result.body = JSON.parse(response.result.body); 130 | this.set('cloudCodeResult', JSON.stringify(response, null, 2)); 131 | }); 132 | }, 133 | 134 | triggerError() { 135 | this.get('cloud').run('notExistingCode') 136 | .catch((response) => { 137 | this.set('triggeredError', JSON.stringify(response, null, 2)); 138 | }); 139 | } 140 | 141 | } 142 | }); 143 | -------------------------------------------------------------------------------- /tests/dummy/app/helpers/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/dummy/app/helpers/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | 12 | 13 | 14 | 15 | 16 | {{content-for 'head-footer'}} 17 | 18 | 19 | {{content-for 'body'}} 20 | 21 | 22 | 23 | 24 | {{content-for 'body-footer'}} 25 | 26 | 27 | -------------------------------------------------------------------------------- /tests/dummy/app/initializers/parse.js: -------------------------------------------------------------------------------- 1 | import ParseInitializer from 'ember-parse/initializers/parse'; 2 | 3 | export default ParseInitializer; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/models/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/dummy/app/models/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/models/car.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string'), 5 | things: DS.hasMany('thing', {async: true}) 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/category.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string') 5 | }); 6 | -------------------------------------------------------------------------------- /tests/dummy/app/models/friend.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string'), 5 | things: DS.hasMany('thing', {async: true}) 6 | }); 7 | -------------------------------------------------------------------------------- /tests/dummy/app/models/thing.js: -------------------------------------------------------------------------------- 1 | import DS from 'ember-data'; 2 | 3 | export default DS.Model.extend({ 4 | name: DS.attr('string'), 5 | age: DS.attr('number'), 6 | createdAt: DS.attr('date'), 7 | category: DS.belongsTo('category', {async: true}), 8 | friends: DS.hasMany('friend', {async: true}), 9 | cars: DS.hasMany('car', {async: true}) 10 | }); 11 | -------------------------------------------------------------------------------- /tests/dummy/app/models/user.js: -------------------------------------------------------------------------------- 1 | import ParseUser from 'ember-parse/models/parse-user'; 2 | 3 | export default ParseUser; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/router.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import config from './config/environment'; 3 | 4 | var Router = Ember.Router.extend({ 5 | location: config.locationType 6 | }); 7 | 8 | Router.map(function() { 9 | }); 10 | 11 | export default Router; 12 | -------------------------------------------------------------------------------- /tests/dummy/app/routes/application.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | 3 | export default Ember.Route.extend({ 4 | model() { 5 | return this.store.findAll('thing'); 6 | }, 7 | 8 | actions: { 9 | reloadData() { 10 | this.store.unloadAll('car'); 11 | this.store.unloadAll('category'); 12 | this.store.unloadAll('friend'); 13 | this.store.unloadAll('thing'); 14 | this.store.unloadAll('user'); 15 | this.refresh(); 16 | } 17 | } 18 | }); 19 | -------------------------------------------------------------------------------- /tests/dummy/app/serializers/application.js: -------------------------------------------------------------------------------- 1 | import ParseSerializer from 'ember-parse/serializers/parse'; 2 | 3 | export default ParseSerializer; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/services/cloud.js: -------------------------------------------------------------------------------- 1 | import CloudCodeService from 'ember-parse/services/cloud'; 2 | 3 | export default CloudCodeService; 4 | -------------------------------------------------------------------------------- /tests/dummy/app/styles/app.css: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/dummy/app/styles/app.css -------------------------------------------------------------------------------- /tests/dummy/app/templates/application.hbs: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 |

Ember Parse

5 | 6 | {{#if isAuthenticated}} 7 |
8 |
9 | {{input type="text" value=username placeholder="email" class="form-control"}} 10 |
11 | 12 | Logout 13 |
14 | {{else}} 15 | {{#if authError}} 16 |

17 | {{authError}} 18 |

19 | {{/if}} 20 | 21 |
22 |
23 | {{input type="text" value=username placeholder="username" class="form-control"}} 24 |
25 |
26 | {{input type="text" value=password placeholder="password" class="form-control"}} 27 |
28 | 29 | 30 |
31 | {{/if}} 32 |
33 |
34 | 35 |
36 | 37 |
38 |
39 | Create Object 40 |

41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | {{#each model as |thing|}} 54 | 55 | 56 | 57 | 58 | 63 | 68 | 71 | 75 | 76 | {{/each}} 77 | 78 |
IDNameCategoryFriendsCarsDate CreatedActions
{{thing.id}}{{thing.name}}{{thing.category.name}} 59 | {{#each thing.friends as |friend|}} 60 | {{friend.name}} Delete
61 | {{/each}} 62 |
64 | {{#each thing.cars as |car|}} 65 | {{car.name}} Update
66 | {{/each}} 67 |
69 | {{thing.createdAt}} 70 | 72 | Delete 73 | Update 74 |
79 |
80 |
81 | 82 |
83 | 84 |
85 |
86 | 87 |

88 | {{#if cloudCodeResult}} 89 |
{{cloudCodeResult}}
90 | {{/if}} 91 |
92 |
93 | 94 |
95 | 96 |
97 |
98 | 99 |

100 | {{#if triggeredError}} 101 |
{{triggeredError}}
102 | {{/if}} 103 |
104 |
105 |
106 | -------------------------------------------------------------------------------- /tests/dummy/app/templates/components/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/dummy/app/templates/components/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/app/views/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/dummy/app/views/.gitkeep -------------------------------------------------------------------------------- /tests/dummy/config/environment.js: -------------------------------------------------------------------------------- 1 | /* jshint node: true */ 2 | 3 | module.exports = function(environment) { 4 | var ENV = { 5 | modulePrefix: 'dummy', 6 | environment: environment, 7 | baseURL: '/', 8 | locationType: 'auto', 9 | EmberENV: { 10 | // RAISE_ON_DEPRECATION: true, 11 | // LOG_STACKTRACE_ON_DEPRECATION: true, 12 | FEATURES: { 13 | // Here you can enable experimental features on an ember canary build 14 | // e.g. 'with-controller': true 15 | } 16 | }, 17 | 18 | APP: { 19 | // Here you can pass flags/options to your application instance 20 | // when it is created 21 | } 22 | }; 23 | 24 | ENV['ember-parse'] = { 25 | PARSE_APPLICATION_ID: 'CIYQmnUpXGDKJiGsySpST80UT0Q6CkPaTdBJlD5T', 26 | PARSE_JAVASCRIPT_KEY: 'hMcPUKC7NDDGtUQyGMqmgnU0IVK3R65NlADbWcMJ', 27 | session: { 28 | authenticationRoute: 'index' 29 | } 30 | }; 31 | 32 | ENV.contentSecurityPolicy = { 33 | 'default-src': "'none'", 34 | 'script-src': "'self'", 35 | 'font-src': "'self' https://cdnjs.cloudflare.com", 36 | 'connect-src': "'self' https://api.parse.com", 37 | 'img-src': "'self'", 38 | 'style-src': "'self' 'unsafe-inline' https://cdnjs.cloudflare.com", 39 | 'media-src': "'self'" 40 | }; 41 | 42 | if (environment === 'development') { 43 | // ENV.APP.LOG_RESOLVER = true; 44 | // ENV.APP.LOG_ACTIVE_GENERATION = true; 45 | // ENV.APP.LOG_TRANSITIONS = true; 46 | // ENV.APP.LOG_TRANSITIONS_INTERNAL = true; 47 | // ENV.APP.LOG_VIEW_LOOKUPS = true; 48 | } 49 | 50 | if (environment === 'test') { 51 | // Testem prefers this... 52 | ENV.baseURL = '/'; 53 | ENV.locationType = 'none'; 54 | 55 | // keep test console output quieter 56 | ENV.APP.LOG_ACTIVE_GENERATION = false; 57 | ENV.APP.LOG_VIEW_LOOKUPS = false; 58 | 59 | ENV.APP.rootElement = '#ember-testing'; 60 | } 61 | 62 | if (environment === 'production') { 63 | ENV.baseURL = '/ember-parse'; 64 | } 65 | 66 | return ENV; 67 | }; 68 | -------------------------------------------------------------------------------- /tests/dummy/public/crossdomain.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | 16 | -------------------------------------------------------------------------------- /tests/dummy/public/robots.txt: -------------------------------------------------------------------------------- 1 | # http://www.robotstxt.org 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /tests/helpers/resolver.js: -------------------------------------------------------------------------------- 1 | import Resolver from 'ember/resolver'; 2 | import config from '../../config/environment'; 3 | 4 | var resolver = Resolver.create(); 5 | 6 | resolver.namespace = { 7 | modulePrefix: config.modulePrefix, 8 | podModulePrefix: config.podModulePrefix 9 | }; 10 | 11 | export default resolver; 12 | -------------------------------------------------------------------------------- /tests/helpers/start-app.js: -------------------------------------------------------------------------------- 1 | import Ember from 'ember'; 2 | import Application from '../../app'; 3 | import Router from '../../router'; 4 | import config from '../../config/environment'; 5 | 6 | export default function startApp(attrs) { 7 | var application; 8 | 9 | var attributes = Ember.merge({}, config.APP); 10 | attributes = Ember.merge(attributes, attrs); // use defaults, but you can override; 11 | 12 | Ember.run(function() { 13 | application = Application.create(attributes); 14 | application.setupForTesting(); 15 | application.injectTestHelpers(); 16 | }); 17 | 18 | return application; 19 | } 20 | -------------------------------------------------------------------------------- /tests/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | Dummy Tests 7 | 8 | 9 | 10 | {{content-for 'head'}} 11 | {{content-for 'test-head'}} 12 | 13 | 14 | 15 | 16 | 17 | {{content-for 'head-footer'}} 18 | {{content-for 'test-head-footer'}} 19 | 20 | 21 | 22 | {{content-for 'body'}} 23 | {{content-for 'test-body'}} 24 | 25 | 26 | 27 | 28 | 29 | 30 | {{content-for 'body-footer'}} 31 | {{content-for 'test-body-footer'}} 32 | 33 | 34 | -------------------------------------------------------------------------------- /tests/test-helper.js: -------------------------------------------------------------------------------- 1 | import resolver from './helpers/resolver'; 2 | import { 3 | setResolver 4 | } from 'ember-qunit'; 5 | 6 | setResolver(resolver); 7 | -------------------------------------------------------------------------------- /tests/unit/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/tests/unit/.gitkeep -------------------------------------------------------------------------------- /tests/unit/adapters/parse-test.js: -------------------------------------------------------------------------------- 1 | /* global module, test, QUnit, start, stop */ 2 | import Ember from 'ember'; 3 | import Adapter from 'ember-parse-adapter/adapters/application'; 4 | import Serializer from 'ember-parse-adapter/serializers/application'; 5 | 6 | var buildContainer = function() { 7 | var container = new Ember.Container(); 8 | DS._setupContainer(container); 9 | 10 | container.register('serializer:-parse', Serializer); 11 | 12 | return container; 13 | }, 14 | get = Ember.get, 15 | set = Ember.set, 16 | adapter, 17 | container, 18 | store, 19 | serializer, 20 | ajaxUrl, 21 | ajaxType, 22 | ajaxHash, 23 | Post, 24 | Comment; 25 | 26 | module('Unit - adapter:application', { 27 | beforeEach: function() { 28 | ajaxUrl = undefined; 29 | ajaxType = undefined; 30 | ajaxHash = undefined; 31 | 32 | container = buildContainer(); 33 | 34 | container.register('adapter:application', Adapter.extend({ 35 | ajax: function(url, method, hash) { 36 | return new Ember.RSVP.Promise(function(res, rej) { 37 | hash = hash || {}; 38 | var success = hash.success; 39 | 40 | hash.context = adapter; 41 | 42 | ajaxUrl = url; 43 | ajaxType = method; 44 | ajaxHash = hash; 45 | 46 | hash.success = function(json) { 47 | Ember.run(function() { 48 | res(json); 49 | }); 50 | }; 51 | 52 | hash.error = function(xhr) { 53 | Ember.run(function() { 54 | rej(xhr); 55 | }); 56 | }; 57 | }); 58 | } 59 | })); 60 | 61 | container.register('model:post', DS.Model.extend({ 62 | title : DS.attr('string'), 63 | comments : DS.hasMany('comment', { async: true }) 64 | })); 65 | 66 | container.register('model:comment', DS.Model.extend({ 67 | content : DS.attr('string'), 68 | post : DS.belongsTo('post', { async: true }) 69 | })); 70 | 71 | store = container.lookup('store:main'); 72 | adapter = container.lookup('adapter:application'); 73 | serializer = container.lookup('serializer:-parse'); 74 | 75 | Post = store.modelFor('post'); 76 | Comment = store.modelFor('comment'); 77 | }, 78 | 79 | afterEach: function() { 80 | Ember.run(container, 'destroy'); 81 | } 82 | }); 83 | 84 | test('find', function(assert) { 85 | var post = Ember.run(function() { 86 | return store.find('post', 'firstPost'); 87 | }), 88 | isLoaded; 89 | 90 | assert.ok(!get(post, 'isLoaded')); 91 | assert.equal(ajaxUrl, 'https://api.parse.com/1/classes/Post/firstPost', 'The Parse API version and classes with Post and ID.'); 92 | assert.equal(ajaxType, 'GET'); 93 | assert.ok(Ember.get(ajaxHash, 'context.headers').hasOwnProperty('X-Parse-REST-API-Key'), 'has rest api header'); 94 | assert.ok(Ember.get(ajaxHash, 'context.headers').hasOwnProperty('X-Parse-Application-Id'), 'has rest application header'); 95 | 96 | ajaxHash.success({ 97 | objectId : 'firstPost', 98 | title : 'Testing Find' 99 | }); 100 | 101 | assert.ok(get(post, 'isLoaded')); 102 | assert.ok(!get(post, 'isDirty')); 103 | 104 | isLoaded = store.recordIsLoaded('post', 'firstPost'); 105 | assert.ok(isLoaded, 'the record is now in the store, and can be looked up by ID without another AJAX request'); 106 | }); 107 | 108 | test('find with sessionToken', function(assert) { 109 | var post = Ember.run(function() { 110 | adapter.set('sessionToken', 'some-odd-token'); 111 | return store.find('post', 'firstPost'); 112 | }), 113 | isLoaded; 114 | 115 | assert.ok(!get(post, 'isLoaded')); 116 | assert.equal(ajaxUrl, 'https://api.parse.com/1/classes/Post/firstPost', 'The Parse API version and classes with Post and ID.'); 117 | assert.equal(ajaxType, 'GET'); 118 | assert.equal(Ember.get(ajaxHash, 'context.headers.X-Parse-Session-Token'), 'some-odd-token', 'has session header'); 119 | 120 | ajaxHash.success({ 121 | objectId: 'firstPost', 122 | title: 'Testing Find' 123 | }); 124 | 125 | assert.ok(get(post, 'isLoaded')); 126 | assert.ok(!get(post, 'isDirty')); 127 | 128 | isLoaded = store.recordIsLoaded('post', 'firstPost'); 129 | assert.ok(isLoaded, 'the record is now in the store, and can be looked up by ID without another AJAX request'); 130 | }); 131 | 132 | test('can get a set sessionToken', function(assert) { 133 | var token = Ember.run(function() { 134 | adapter.set('sessionToken', 'some-odd-token'); 135 | return adapter.get('sessionToken'); 136 | }); 137 | 138 | assert.equal(token, 'some-odd-token'); 139 | }); 140 | 141 | test('findAll', function(assert) { 142 | var posts, 143 | post, 144 | isLoaded; 145 | 146 | posts = store.find('post'); 147 | 148 | assert.expect(ajaxUrl, '/1/classes/Post', 'The Parse API version and classes with Post.'); 149 | assert.equal(ajaxType, 'GET'); 150 | 151 | // Parse REST API wraps the collections in a results JSON label. 152 | ajaxHash.success({ 153 | results: [ 154 | { objectId: '1', title: 'First Post.' }, 155 | { objectId: '2', title: 'Second Post.' } 156 | ] 157 | }); 158 | 159 | 160 | posts.then(function(_posts) { 161 | posts = _posts; 162 | }); 163 | 164 | Ember.run(function() { 165 | assert.equal(get(posts, 'length'), 2); 166 | }); 167 | 168 | post = posts.objectAt(0); 169 | 170 | assert.ok(get(post, 'isLoaded')); 171 | assert.ok(!get(post, 'isDirty')); 172 | 173 | isLoaded = store.recordIsLoaded('post', '1'); 174 | 175 | assert.ok(isLoaded, 'the record is now in the store, and can be looked up by ID without another Ajax request'); 176 | }); 177 | 178 | QUnit.skip('findMany via a hasMany relationship', function(assert) { 179 | Ember.run(function() { 180 | store.push('post', { id: 'one', comments: ['aa1', 'bb2', 'cc3'] }); 181 | }); 182 | 183 | Ember.run(function() { 184 | store.find('post', 'one').then(function(post) { 185 | return get(post, 'comments'); 186 | }).then(function(comments) { 187 | var comment1, 188 | comment2, 189 | comment3; 190 | 191 | assert.equal(get(comments, 'length'), 3, 'there are three comments in the relationship already'); 192 | 193 | comment1 = comments.objectAt(0); 194 | assert.equal(get(comment1, 'id'), 'aa1'); 195 | 196 | comment2 = comments.objectAt(1); 197 | assert.equal(get(comment2, 'id'), 'bb2'); 198 | 199 | comment3 = comments.objectAt(2); 200 | assert.equal(get(comment3, 'id'), 'cc3'); 201 | 202 | comments.forEach(function(comment) { 203 | assert.equal(get(comment, 'isLoaded'), true, 'comment is loaded'); 204 | }); 205 | }); 206 | }); 207 | 208 | assert.expect(ajaxUrl, '/1/classes/Comment', 'requests the comment class'); 209 | assert.equal(ajaxType, 'POST'); 210 | assert.deepEqual(ajaxHash.data, { where: { objectId: { '$in': 'aa1,bb2,cc3' } } }, 'the hash was passed along'); 211 | 212 | ajaxHash.success({ results: [ 213 | { objectId: 'aa1', content: 'Comment 1' }, 214 | { objectId: 'bb2', content: 'Comment 2' }, 215 | { objectId: 'cc3', content: 'Comment 3' } 216 | ] }); 217 | }); 218 | 219 | test('Find Query with non-string where', function(assert) { 220 | var posts = store.find('post', { where: { title: 'First Post' } }); 221 | 222 | assert.equal(get(posts, 'length'), 0, 'there are no posts yet as the query has not returned.'); 223 | assert.expect(ajaxUrl, '/1/classes/Post', 'requests the post class'); 224 | assert.equal(ajaxType, 'GET'); 225 | assert.deepEqual(ajaxHash.data, { where: JSON.stringify({ title: 'First Post' }) }, 'where clause is passed as stringified data'); 226 | 227 | ajaxHash.success({ results: [ 228 | { objectId: 'bad1', title: 'First Post' }, 229 | { objectId: 'bad2', title: 'First Post' } 230 | ] }); 231 | }); 232 | 233 | test('Find Query with where as string', function(assert) { 234 | var posts = store.find('post', { where: "{title: 'First Post'}" }); 235 | 236 | assert.equal(get(posts, 'length'), 0, 'there are no posts yet as the query has not returned.'); 237 | assert.expect(ajaxUrl, '/1/classes/Post', 'requests the post class'); 238 | assert.equal(ajaxType, 'GET'); 239 | assert.deepEqual(ajaxHash.data, { where: "{title: 'First Post'}" }, 'where clause is passed through as string'); 240 | 241 | ajaxHash.success({ results: [ 242 | { objectId: 'bad1', title: 'First Post' }, 243 | { objectId: 'bad2', title: 'First Post' } 244 | ] }); 245 | 246 | assert.equal(get(posts, 'length'), 2, 'there are 2 posts loaded'); 247 | 248 | posts.forEach(function(post) { 249 | assert.equal(get(post, 'isLoaded'), true, 'the post is being loaded'); 250 | }); 251 | }); 252 | 253 | test('Create Record', function(assert) { 254 | stop(); 255 | 256 | var post, 257 | promise; 258 | 259 | Ember.run(function() { 260 | post = store.createRecord('post', { title: 'Testing Create' }); 261 | assert.ok(get(post, 'isNew'), 'record is new'); 262 | promise = post.save(); 263 | }); 264 | 265 | assert.ok(get(post, 'isSaving'), 'record is saving'); 266 | assert.equal(ajaxUrl, 'https://api.parse.com/1/classes/Post', 'requests the post class'); 267 | assert.equal(ajaxType, 'POST'); 268 | 269 | // Passing comments as an Ember array. This is due to a bug in Ember-Data 270 | // expecting an Ember array for data and not a raw array: 271 | // https://github.com/emberjs/data/pull/1939 272 | assert.deepEqual(ajaxHash.data, { comments: Ember.A(), title: 'Testing Create' }, 'raw data is posted'); 273 | 274 | ajaxHash.success({ 275 | objectId : 'created321', 276 | createdAt : (new Date()).toISOString() 277 | }); 278 | 279 | Ember.run(function() { 280 | promise.then(function() { 281 | assert.ok(!get(post, 'isSaving'), 'post is not saving after save'); 282 | assert.ok(!get(post, 'isDirty'), 'post is not dirty after save'); 283 | start(); 284 | }); 285 | }); 286 | }); 287 | 288 | QUnit.skip('Create Record - bulkCommit', function(assert) { 289 | var posts = new Ember.Set([ 290 | store.createRecord(Post, { title: 'Post 1' }), 291 | store.createRecord(Post, { title: 'Post 2' }) 292 | ]), 293 | expectStates, 294 | expectUrl, 295 | expectType, 296 | expectData, 297 | expect; 298 | 299 | expectStates(posts, 'new'); 300 | store.commit(); 301 | 302 | expectStates(posts, 'saving'); 303 | expectUrl('/1/batch'); 304 | expectType('POST'); 305 | 306 | 307 | //This payload should match expected schema: https://www.parse.com/docs/rest#objects-batch 308 | expectData({ requests: [ 309 | { 310 | method: 'POST', 311 | path: '/1/classes/Post', 312 | body: { comments: [], title: 'Post 1', updatedAt: undefined, createdAt: undefined } 313 | }, 314 | { 315 | method: 'POST', 316 | path: '/1/classes/Post', 317 | body: { comments: [], title: 'Post 2', updatedAt: undefined, createdAt: undefined } 318 | } 319 | ] }); 320 | 321 | ajaxHash.success([ 322 | { success: { objectId: 'post1', createdAt: (new Date()).toISOString() } }, 323 | { success: { objectId: 'post2', createdAt: (new Date()).toISOString() } } 324 | ]); 325 | 326 | expectStates(posts, 'saving', false); 327 | expect(posts[0], store.find(Post, 'post1'), 'should match first post.'); 328 | expect(posts[1], store.find(Post, 'post2'), 'should match second post.'); 329 | }); 330 | 331 | QUnit.skip('Update Record - not bulkCommit', function(assert) { 332 | store.load(Post, { title: 'Test Post Update', objectId: 'postUpdated' }); 333 | 334 | // force it to use single record update 335 | adapter.bulkCommit = false; 336 | var post = store.find(Post, 'postUpdated'), 337 | expectState, 338 | expectUrl, 339 | expectType, 340 | expectData; 341 | 342 | expectState('loaded'); 343 | expectState('dirty', false); 344 | post.set('title', 'Test Post Updated - true'); 345 | expectState('dirty'); 346 | store.commit(); 347 | expectState('saving'); 348 | expectUrl('/1/classes/Post/postUpdated'); 349 | expectType('PUT'); 350 | expectData({ objectId: 'postUpdated', comments: [], title: 'Test Post Updated - true', updatedAt: undefined, createdAt: undefined }); 351 | ajaxHash.success({ objectId: 'postUpdated', updatedAt: (new Date()).toISOString() }); 352 | expectState('saving', false); 353 | }); 354 | 355 | QUnit.skip('Update Record - bulkCommit', function(assert) { 356 | store.loadMany(Post, [ 357 | { objectId: 'post1', title: 'Post 1' }, 358 | { objectId: 'post2', title: 'Post 2' } 359 | ]); 360 | 361 | var posts = store.findMany(Post, ['post1', 'post2']), 362 | expectStates, 363 | expectUrl, 364 | expectType, 365 | expectData, 366 | expect; 367 | 368 | expectStates(posts, 'loaded'); 369 | posts.forEach(function(post) { 370 | post.set('title', post.get('title') + ' updated.'); 371 | }); 372 | expectStates(posts, 'dirty'); 373 | store.commit(); 374 | expectStates(posts, 'saving'); 375 | expectUrl('/1/batch'); 376 | expectType('POST'); 377 | expectData({ 378 | requests: [{ 379 | method: 'PUT', 380 | path: '/1/classes/Post/post1', 381 | body: { objectId: 'post1', comments: [], title: 'Post 1 updated.', updatedAt: undefined, createdAt: undefined } 382 | }, 383 | { 384 | method: 'PUT', 385 | path: '/1/classes/Post/post2', 386 | body: { objectId: 'post2', comments: [], title: 'Post 2 updated.', updatedAt: undefined, createdAt: undefined } 387 | }] 388 | }); 389 | ajaxHash.success([ 390 | { success: { objectId: 'post1', updatedAt: (new Date()).toISOString() } }, 391 | { success: { objectId: 'post2', updatedAt: (new Date()).toISOString() } } 392 | ]); 393 | expectStates(posts, 'saving', false); 394 | expect(posts[0], store.find(Post, 'post1'), 'should match first post.'); 395 | expect(posts[1], store.find(Post, 'post2'), 'should match second post.'); 396 | }); 397 | 398 | QUnit.skip('Delete Record - not bulkCommit', function(assert) { 399 | store.load(Post, { objectId: 'post1', title: 'Post to delete.' }); 400 | // force single record delete 401 | adapter.bulkCommit = false; 402 | var post = store.find(Post, 'post1'), 403 | expectState, 404 | expectUrl, 405 | expectType; 406 | 407 | expectState('new', false); 408 | expectState('loaded'); 409 | expectState('dirty', false); 410 | post.deleteRecord(); 411 | expectState('dirty'); 412 | expectState('deleted'); 413 | store.commit(); 414 | expectState('saving'); 415 | expectUrl('/1/classes/Post/post1'); 416 | expectType('DELETE'); 417 | ajaxHash.success(); 418 | expectState('deleted'); 419 | }); 420 | 421 | QUnit.skip('Delete Record - bulkCommit', function(assert) { 422 | store.loadMany(Post, [ 423 | { objectId: 'post1', title: 'Post 1' }, 424 | { objectId: 'post2', title: 'Post 2' } 425 | ]); 426 | var posts = store.findMany(Post, ['post1', 'post2']), 427 | expectStates, 428 | expectUrl, 429 | expectType, 430 | expectData; 431 | 432 | expectStates(posts, 'loaded'); 433 | expectStates(posts, 'new', false); 434 | expectStates(posts, 'dirty', false); 435 | posts.forEach(function(post) { 436 | post.deleteRecord(); 437 | }); 438 | expectStates(posts, 'dirty'); 439 | expectStates(posts, 'deleted'); 440 | store.commit(); 441 | expectStates(posts, 'saving'); 442 | expectUrl('/1/batch'); 443 | expectType('POST'); 444 | expectData({ 445 | requests: [{ 446 | method: 'DELETE', 447 | path: '/1/classes/Post/post1', 448 | body: { objectId: 'post1', comments: [], title: 'Post 1', updatedAt: undefined, createdAt: undefined } 449 | }, 450 | { 451 | method: 'DELETE', 452 | path: '/1/classes/Post/post2', 453 | body: { objectId: 'post2', comments: [], title: 'Post 2', updatedAt: undefined, createdAt: undefined } 454 | }] 455 | }); 456 | ajaxHash.success(); 457 | expectStates(posts, 'saving', false); 458 | expectStates(posts, 'dirty', false); 459 | }); 460 | -------------------------------------------------------------------------------- /vendor/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/blimpllc/ember-parse/7c80c1fec293591857a8ab05a9197b829d01839a/vendor/.gitkeep --------------------------------------------------------------------------------