diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..5ecf4a0 --- /dev/null +++ b/.gitignore @@ -0,0 +1,5 @@ +# Mac OS X +.DS_Store + +# npm +/node_modules/ diff --git a/lib/Parse.js b/lib/Parse.js index 14c8b88..22e73e7 100644 --- a/lib/Parse.js +++ b/lib/Parse.js @@ -1,118 +1,477 @@ var qs = require('querystring'); +qs.escape = function(q){ return q; }; + module.exports = Parse; -function Parse(application_id, master_key) { - this._application_id = application_id; - this._master_key = master_key; +function Parse(options_or_application_id, master_key) { + this._options = {}; + if (master_key) { + this._options.app_id = options_or_application_id; + this._options.master_key = master_key; + } else { + this._options = options_or_application_id; + } } Parse.prototype = { - _api_protocol: require('https'), - _api_host: 'api.parse.com', - _api_port: 443, - - // add object to class store - insert: function (className, object, callback) { - parseRequest.call(this, 'POST', '/1/classes/' + className, object, callback); - }, - - // get objects from class store - find: function (className, query, callback) { - if (typeof query === 'string') { - parseRequest.call(this, 'GET', '/1/classes/' + className + '/' + query, null, callback); - } else { - parseRequest.call(this, 'GET', '/1/classes/' + className, { where: JSON.stringify(query) }, callback); + _api_protocol: require('https'), + _api_host: 'api.parse.com', + _api_port: 443, + + // add object to class store + insert: function (className, object, callback) { + parseRequest.call(this, 'POST', '/1/classes/' + className, object, callback); + }, + + // add files + insertFile: function(fileName, data, contentType, callback){ + parseRequest.call(this, 'POST', '/1/files/' + fileName, data, callback, contentType); + }, + + // create a new user + insertUser: function (data, callback) { + parseRequest.call(this, 'POST', '/1/users/', data, callback); + }, + + findWithObjectId: function (className, objectId, callback){ + var url = '/1/' + (className === '_User' ? 'users' : 'classes/' + className + '/' + objectId); + + + parseRequest.call(this, 'GET', url, null, callback); + + }, + // get an object from the class store + find: function (className, query, callback) { + var url = '/1/' + (className === '_User' ? 'users' : 'classes/' + className); + var queryType = typeof query; + if ( queryType === 'string' ) { + url += '/' + query; + } else if ( queryType === 'object' ) { + + // if the user wants to add 'include' or 'key' (or other types of) constraints while getting only one object + // objectId can be added to the query object and is deleted after it's appended to the url + if ( query.hasOwnProperty('objectId') ) { + url += '/' + query.objectId; + delete query.objectId; + } + + // check to see if there is a 'where' object in the query object + // the 'where' object need to be stringified by JSON.stringify(), not querystring + if ( query.hasOwnProperty('where') ) { + url += '?where=' + encodeURIComponent(JSON.stringify(query.where)); + delete query.where; + } + + // if there are no more constraints left in the query object 'remainingQuery' will be an empty string + var remainingQuery = qs.stringify(query); + if ( remainingQuery ) { + url += ( url.indexOf('?') === -1 ? '?' : '&' ) + remainingQuery; + } + + } + parseRequest.call(this, 'GET', url, null, callback); + }, + + // get a collection of objects + findMany: function (className, query, order, limit, skip, callback) { + console.warn('"findMany" is deprecated, use "find" instead.'); + + switch ( arguments.length ) { + case 3: callback = order; + break; + case 5: query.order = order; + query.limit = limit; + callback = skip; + break; + case 6: query.order = order; + query.limit = limit; + query.skip = skip; + break; + default: throw new Error('Unexpected number of arguments.'); + } + + this.find(className, query, callback); + }, + + // do a batch of requests at once + batch: function (requests,callback) { + parseRequest.call(this,'POST','/1/batch',{requests:requests},callback); + }, + + // user login + loginUser: function (username, password, callback) { + parseRequest.call(this, 'GET', '/1/login/?username=' + username + '&password=' + password, null, callback); + }, + + // user logout + logoutUser: function (sessionToken, callback) { + parseRequest.call(this, 'POST', '/1/logout', null, callback, null, sessionToken); + }, + + // retrieve current user + me: function (sessionToken, callback) { + parseRequest.call(this, 'GET', '/1/users/me', null, callback, null, sessionToken); + }, + + // retrieve contents of one or more user objects + findUser: function (query, callback) { + if ( arguments.length !== 2 ) { + throw new Error('Unexpected number of arguments.'); + } else { + this.find('_User', query, callback); + } + }, + + // get an object belonging to a certain User + findFileByUser: function(userId, className, callback) { + queryString = 'where={"user":' + '"' + userId + '"' + '}'; + encodedString = encodeURIComponent(queryString); + parseRequest.call(this, 'GET', '/1/classes/' + className + '?' + encodedString, null, callback); + }, + + getUser: function (query, callback, deprecatedCallback) { + console.warn('getUser is deprecated, user findUser instead.'); + if ( arguments.length === 3 ) { + console.warn('Logging in with "getUser" is deprecated, use loginUser instead.'); + console.warn('Use "findUser" to retrieve one or more user object contents.'); + this.loginUser(query, callback, deprecatedCallback); + } else { + this.findUser(query, callback); + } + }, + + getFileByUser: function(userId, className, callback) { + console.warn('getFileByUser is deprecated, user findFileByUser instead.'); + this.findFileByUser(userId, className, callback); + }, + + // insert an object into Parse + insertCustom: function (className, object, callback) { + parseRequest.call(this, 'POST', '/1/' + className, object, callback); + }, + + // update an object in the class store + update: function (className, objectId, object, callback) { + parseRequest.call(this, 'PUT', '/1/classes/' + className + '/' + objectId, object, callback); + }, + + // update a User object's email address + updateUserEmail: function(objectId, data, sessionToken, callback) { + switch ( arguments.length ) { + case 3: callback = sessionToken; + break; + case 4: break; + default: throw new Error('Unexpected number of arguments.'); + } + data = { email: data }; + parseRequest.call(this, 'PUT', '/1/users/' + objectId, data, callback, null, sessionToken); + }, + + // update a User object's username* + updateUserName: function(objectId, data, sessionToken, callback) { + switch ( arguments.length ) { + case 3: callback = sessionToken; + break; + case 4: break; + default: throw new Error('Unexpected number of arguments.'); + } + data = { username: data }; + parseRequest.call(this, 'PUT', '/1/users/' + objectId, data, callback, null, sessionToken); + }, + + // update any keys of a User object + updateUser: function (objectId, data, sessionToken, callback) { + switch ( arguments.length ) { + case 3: callback = sessionToken; + break; + case 4: break; + default: throw new Error('Unexpected number of arguments.'); + } + parseRequest.call(this, 'PUT', '/1/users/' + objectId, data, callback, null, sessionToken); + }, + + // reset a User object's password + passwordReset: function (data, callback) { + data = { email: data }; + parseRequest.call(this, 'POST', '/1/requestPasswordReset/', data, callback); + }, + + // remove an object from the class store + delete: function (className, objectId, callback) { + parseRequest.call(this, 'DELETE', '/1/classes/' + className + '/' + objectId, null, callback); + }, + + // remove an object from the class store + deleteUser: function (objectId, sessionToken, callback) { + switch ( arguments.length ) { + case 2: callback = sessionToken; + break; + case 3: break; + default: throw new Error('Unexpected number of arguments'); + } + parseRequest.call(this, 'DELETE', '/1/users/' + objectId, null, callback, null, sessionToken); + }, + + deleteAll: function(className, callback){ + var that = this; + this.find(className, '', function (err, response) { + var requests = toDeleteOps(className, response.results); + that.batch(requests, callback); + }); + }, + + deleteAllUsers: function(callback){ + var that = this; + this.find('_User', '', function (err, response) { + var requests = toDeleteOps('_User', response.results); + that.batch(requests, callback); + }); + }, + + // upload installation data + insertInstallationData: function (deviceType, deviceToken, callback) { + if (deviceType === 'ios'){ + data = { deviceType: deviceType, deviceToken: deviceToken }; + } + else { + data = { deviceType: deviceType, installationId: deviceToken }; + } + parseRequest.call(this, 'POST', '/1/installations/', data, callback); + }, + + insertInstallationDataWithTimeZone: function (deviceType, deviceToken, timeZone, callback) { + if (deviceType === 'ios'){ + data = { deviceType: deviceType, deviceToken: deviceToken, timeZone: timeZone }; + } + else { + data = { deviceType: deviceType, installationId: deviceToken, timeZone: timeZone }; + } + parseRequest.call(this, 'POST', '/1/installations/', data, callback); + }, + + insertInstallationDataWithChannels: function (deviceType, deviceToken, channels, callback) { + if (deviceType === 'ios'){ + data = { deviceType: deviceType, deviceToken: deviceToken, channels: channels }; + } + else { + data = { deviceType: deviceType, installationId: deviceToken, channels: channels }; + } + parseRequest.call(this, 'POST', '/1/installations/', data, callback); + }, + + insertInstallationDataWithTimeZoneAndChannels: function (deviceType, deviceToken, timeZone, channels, callback) { + if (deviceType === 'ios'){ + data = { deviceType: deviceType, deviceToken: deviceToken, timeZone: timeZone, channels: channels }; + } + else { + data = { deviceType: deviceType, installationId: deviceToken, timeZone: timeZone, channels: channels }; + } + parseRequest.call(this, 'POST', '/1/installations/', data, callback); + }, + + updateInstallationDataChannels: function(objectId, channels, callback) { + parseRequest.call(this, 'PUT', '/1/installations/' + objectId, { + channels: channels + }, callback); + }, + + getInstallationData: function(callback) { + parseRequest.call(this, 'GET', '/1/installations', null, callback); + }, + + getInstallationDataForDeviceToken: function(deviceToken, callback) { + parseRequest.call(this, 'GET', '/1/installations?where={"deviceToken":"'+deviceToken+'"}', null, callback); + }, + + deleteInstallation: function(objectId, callback) { + parseRequest.call(this, 'DELETE', '/1/installations/' + objectId, null, callback); + }, + + upsertInstallation: function(deviceType, deviceToken, data, callback) { + data.deviceType = deviceType; + data.deviceToken = deviceToken; + parseRequest.call(this, 'POST', '/1/installations', data, callback); + }, + + insertOrUpdateInstallationDataWithChannels: function(deviceType, deviceToken, channels, callback) { + var that = this; + this.getInstallationDataForDeviceToken(deviceToken, function(err, results) { + if (!err && results.results.length) { + that.updateInstallationDataChannels(results.results[0].objectId, channels); + return; + } + that.insertInstallationDataWithChannels(deviceType, deviceToken, channels, callback); + }); + }, + + countObjects: function (className, query, callback) { + if (typeof(query) === "function") { + parseRequest.call(this, 'GET', '/1/classes/' + className, null, query); + } + if (typeof(query) === "string") { + parseRequest.call(this, 'GET', '/1/classes/' + className + '/' + query, null, callback); + } else { + parseRequest.call(this, 'GET', '/1/classes/' + className, { where: JSON.stringify(query) }, callback); + } + }, + + addRelation: function( relationName, className1, objectId1, className2, objectId2, callback) { + data = {}; + data[relationName] = { __op:"AddRelation",objects:[{__type:"Pointer",className:className2,objectId:objectId2}]}; + parseRequest.call(this,'PUT','/1/classes/' + className1+'/'+objectId1,data,callback); + }, + + removeRelation: function( relationName, className1, objectId1, className2, objectId2, callback) { + data = {}; + data[relationName] = { __op:"RemoveRelation",objects:[{__type:"Pointer",className:className2,objectId:objectId2}]}; + parseRequest.call(this,'PUT','/1/classes/' + className1+'/'+objectId1,data,callback); + }, + + + insertRole: function (data, callback) { + parseRequest.call(this, 'POST', '/1/roles/', data, callback); + }, + + getRole: function (objectId, callback) { + parseRequest.call(this, 'GET', '/1/roles/' + objectId, null, callback); + }, + + getRoles: function (params, callback) { + if (typeof(params) === "function") { + parseRequest.call(this, 'GET', '/1/roles/', null, params); + } + if (typeof(params) === "string") { + parseRequest.call(this, 'GET', '/1/roles/' + params, null, callback); + } else { + params = JSON.stringify(params); + parseRequest.call(this, 'GET', '/1/roles/', params, callback); + } + }, + + updateRole: function (objectId, data, callback) { + parseRequest.call(this, 'PUT', '/1/roles/' + objectId, data, callback); + }, + + deleteRole: function (objectId, callback) { + parseRequest.call(this, 'DELETE', '/1/roles/' + objectId, callback); + }, + + sendPush: function (data, callback) { + parseRequest.call(this, 'POST', '/1/push/', data, callback); + }, + + getCurrentSession: function(objectId, callback) + { + parseRequest.call(this, 'GET', '/1/sessions/'+ objectId, callback); } - }, - - // update an object in the class store - update: function (className, objectId, object, callback) { - parseRequest.call(this, 'PUT', '/1/classes/' + className + '/' + objectId, object, callback); - }, - - // remove an object from the class store - 'delete': function (className, objectId, callback) { - parseRequest.call(this, 'DELETE', '/1/classes/' + className + '/' + objectId, null, callback); - } + }; // Parse.com https api request -function parseRequest(method, path, data, callback) { - var auth = 'Basic ' + new Buffer(this._application_id + ':' + this._master_key).toString('base64'); - var headers = { - Authorization: auth, - Connection: 'Keep-alive' - }; - var body = null; - - switch (method) { - case 'GET': - if (data) { - path += '?' + qs.stringify(data); - } - break; - case 'POST': - case 'PUT': - body = JSON.stringify(data); - headers['Content-type'] = 'application/json'; - headers['Content-length'] = body.length; - break; - case 'DELETE': - headers['Content-length'] = 0; - break; - default: - throw new Error('Unknown method, "' + method + '"'); - } - - var options = { - host: this._api_host, - port: this._api_port, - headers: headers, - path: path, - method: method - }; - - var req = this._api_protocol.request(options, function (res) { - if (!callback) { - return; +function parseRequest(method, path, data, callback, contentType, sessionToken) { + var headers = { + Connection: 'Keep-alive' + }; + + if(this._options.master_key){ + var auth = 'Basic ' + new Buffer(this._options.app_id + ':' + this._options.master_key).toString('base64'); + headers.Authorization = auth; + if ( sessionToken ) { + throw new Error('Can\'t use session tokens while using the master key.'); + } + }else if(this._options.api_key){ + headers['X-Parse-Application-Id'] = this._options.app_id; + headers['X-Parse-REST-API-Key'] = this._options.api_key; + if ( sessionToken ) { + headers['X-Parse-Session-Token'] = sessionToken; + } } - - if (res.statusCode < 200 || res.statusCode >= 300) { - var err = new Error('HTTP error ' + res.statusCode); - err.arguments = arguments; - err.type = res.statusCode; - err.options = options; - err.body = body; - return callback(err); + + if(!sessionToken && this._options && this._options.session_token){ + headers['X-Parse-Session-Token'] = this._options.session_token; } - - var json = ''; - res.setEncoding('utf8'); - - res.on('data', function (chunk) { - json += chunk; + + + var body = null; + + switch (method) { + case 'GET': + if (data) { + path += (path.indexOf("?") == -1 ? '?' : '&') + qs.stringify(data); + } + break; + case 'POST': + case 'PUT': + body = contentType ? data : typeof data === 'object' ? JSON.stringify(data) : data; + if ( !contentType ) { + headers['Content-length'] = Buffer.byteLength(body); + } + headers['Content-type'] = contentType || 'application/json'; + break; + case 'DELETE': + headers['Content-length'] = 0; + break; + default: + throw new Error('Unknown method, "' + method + '"'); + } + + var options = { + hostname: this._api_host, + port: this._api_port, + headers: headers, + path: path, + method: method + }; + + var req = this._api_protocol.request(options, function (res) { + if (!callback) { + return; + } + + var json = ''; + res.setEncoding('utf8'); + + res.on('data', function (chunk) { + json += chunk; + }); + + res.on('end', function () { + var data; + try { + data = JSON.parse(json); + if ( data.code || data.error ) { + throw (data); + } + callback(null, data); + } + catch (err) { + callback(err); + } + }); + + res.on('close', function (err) { + callback(err); + }); }); - - res.on('end', function () { - var err = null; - var data = null; - try { - var data = JSON.parse(json); - } catch (err) { - } - callback(err, data); + + req.setTimeout(5000, function () { + req.connection.destroy(); + }); + + body && req.write(body, contentType ? 'binary' : 'utf8'); + req.end(); + + req.on('error', function (err) { + callback && callback(err); }); - - res.on('close', function (err) { - callback(err); +} + +function toDeleteOps(className, objects){ + return objects.map(function(object){ + return { + method: 'DELETE', + path: '/1/' + ( className === '_User' ? 'users/' : 'classes/' + className + '/' ) + object.objectId + }; }); - }); - - body && req.write(body); - req.end(); - - req.on('error', function (err) { - callback && callback(err); - }); } diff --git a/makefile b/makefile index 95ac4b7..17fc7b2 100644 --- a/makefile +++ b/makefile @@ -1,4 +1,4 @@ test: nodeunit nodeunit: - nodeunit test/Parse.test.js + ./node_modules/.bin/nodeunit test/Parse.test.js diff --git a/package.json b/package.json index f130dd0..1451d9a 100644 --- a/package.json +++ b/package.json @@ -1,9 +1,44 @@ { - "name": "parse-api", - "description": "API for accessing Parse.com applications", - "version": "0.1.0", - "author": "Chris Johnson ", - "repository": "git://github.com/tenorviol/node-parse-api", + "name": "node-parse-api", + "description": "A Parse.com REST API client for Node.js", + "version": "0.3.8a", + "author": "Chris Johnson , Michael Leveton , Seth Gholson", + "contributors": [ + "Daniel Gasienica ", + "Jakub Knejzlík", + "Sam Saccone", + "Arnaud Rinquin", + "Charles Julian Knight", + "Barrington Haynes", + "Christian Monaghan", + "Christoffer Niska", + "Rodrigo Martell", + "Joe Bruggeman", + "Omar A", + "Andrey", + "Rafael Galdêncio", + "Akhmad Fathonih", + "Kody J. Peterson", + "Jo Jordens", + "Appsaloon", + "Achim Koellner" + ], "main": "index", - "engines": { "node": ">= 0.4.0" } -} \ No newline at end of file + "scripts": { + "test": "nodeunit" + }, + "repository": "https://github.com/leveton/node-parse-api", + "keywords": [ + "parse", + "mbaas", + "rest", + "push notifications", + "api" + ], + "engines": { + "node": ">= 0.4.0" + }, + "devDependencies": { + "nodeunit": "~0.9.0" + } +} diff --git a/readme.md b/readme.md index c7912ea..0bbcdb8 100644 --- a/readme.md +++ b/readme.md @@ -4,49 +4,562 @@ Node Parse API install ------- - npm install parse-api +``` +npm install node-parse-api +``` examples -------- -### setup +### setup with MASTER_KEY - var Parse = require('parse-api').Parse; - - var APP_ID = ...; - var MASTER_KEY = ...; - - var app = new Parse(APP_ID, MASTER_KEY); +```javascript +var Parse = require('node-parse-api').Parse; -### insert +var APP_ID = ...; +var MASTER_KEY = ...; - // add a Foo object, { foo: 'bar' } - app.insert('Foo', { foo: 'bar' }, function (err, response) { - console.log(response); - }); +var app = new Parse(APP_ID, MASTER_KEY); +``` + +### setup with API_KEY + +```javascript +var Parse = require('node-parse-api').Parse; + +var options = { + app_id:'...', + api_key:'...' // master_key:'...' could be used too +} + +var app = new Parse(options); +``` + +### insert an object + +* insert(className `string`, data `object`, callback `function`) + +```javascript +// add a Foo object, { foo: 'bar' } +app.insert('Foo', { foo: 'bar' }, function (err, response) { + console.log(response); +}); +``` + +### insert a User + +* insertUser(data `object`, callback `function`) + +```javascript +app.insertUser({ + username: 'foo', + password: 'bar' +}, function (err, response) { + console.log(response); +}); +``` + +More properties can be provided, but username and password are required. + +### insert a User with a Pointer + +```javascript +app.insertUser({ + username: 'foo', + password: 'bar', + pointer/*can have any name*/: { + __type: 'Pointer', + className: , + objectId: + } + }, function (err, response) { + console.log(response); +}); +``` + +### insert a User with GeoPoints + +```javascript +app.insertUser({ + username: 'foo', + password: 'bar', + location: { + __type: 'GeoPoint', + latitude: , + longitude: + } + }, function (err, response) { + console.log(response); +}); +``` + +### user login + +* loginUser(username `string`, password `string`, callback `function`) + +Response contains all of the user fields except password, also includes a sessionToken for this user. + +```javascript +app.loginUser('foo', 'bar', function (error, response) { + // response = {sessionToken: '', createdAt: '', ... } + }); +``` + +### me + +* me(sessionToken `string`, callback `function`) + +```javascript +app.me('sessionToken', function (error, response) { + // response is same as getUser response +}); +``` + +### insert a file + +* insertFile(fileName `string`, data `string/buffer`, contentType `string`, callback `function`) + +```javascript +// first upload the file to the parse cloud +app.insertFile('foo.txt', 'bar', 'text/plain', function (err, response) { + // then insert a new object with the link to the new file + app.insert('MyFile', {__type: 'File', "name": response.name }, function (error, response) { + }); +}); +``` ### find one - // the Foo with id = 'someId' - app.find('Foo', 'someId', function (err, response) { - console.log(response); - }); +* find(className `string`, query `object`, callback `function`) + +```javascript +// the Foo with id = 'someId' +app.find('Foo', {objectId: 'someId'}, function (err, response) { + console.log(response); +}); +``` + +Returned fields can be restricted with the 'keys' query. + +```javascript +var query = { + objectId: 'someId', + keys: 'foo,bar' +}; +app.find('Foo', query, function (error, response) { + //response object will only contain foo and bar fields, as well as the special built-in fields (objectId, createdAt and updatedAt) + }); +``` ### find many - // all Foo objects with foo = 'bar' - app.find('Foo', { foo: 'bar' }, function (err, response) { - console.log(response); - }); +* find(className `string`, query `object`, callback `function`) + +```javascript +// all Foo objects with foo = 'bar' +app.find('Foo', {where: {foo: 'bar'}}, function (err, response) { + console.log(response); +}); + +// all Foo objects +// '', null, undefined or any other falsy value will work +app.find('Foo', '', function (err, response) { + console.log(response); +}): +``` + +All types of query constraints Parse provides can be added to the query object as properties. (order, limit, keys, count, include...) + +```javascript +var query = { + where: { + foo: 'bar', + baz: 'qux' + }, + limit: 10, + skip: 5, + order: '-createdAt' +}; +app.find('Foo', query, function (error, response ) { + // the first 5 results will be ignored and the next 10 results will be returned + // response.results will contain up to 10 objects with foo = 'bar' and baz = 'qux', sorted from latest to oldest + }); +``` + +### find one user + +* getUser(query `object`, callback `function`) + +```javascript +app.getUser({objectId: 'someId'}, function (err, response) { + console.log(response); +}); +``` + +### find many users + +* getUser(query `object`, callback `function`) + +```javascript +// all users with foo = 'bar' +app.find({where: {foo: 'bar'}}, function (err, response) { + console.log(response); +}); + +// all users +// '', null, undefined or any other falsy value will work +app.find('', function (err, response) { + console.log(response); +}): +``` + +### count the number of objects + +```javascript + +var query = { + count: 1, + limit: 0 +}; +app.find('Foo', query, function (error, response) { + // { + // results: [], + // count: 123 + // } +}); +``` + +### edit an object + +* update(className `string`, objectId `string`, callback `function`) + +```javascript +app.update('Foo', 'someId', {foo: 'bar'}, function (err, response) { + console.log(response); +}); +``` + +### delete an object + +* delete(className `string`, objectId `string`, callback `function`) + +```javascript +app.delete('Foo', 'someId', function (err, response) { + // response = {} +}); +``` + +### delete all objects in a class + +* deleteAll(className `string`, callback `function`) + +```javascript +app.deleteAll('Foo', function (err, response) { + // response = [{success: {}, success: {}, ... }] +}); +``` + +### delete user + +* deleteUser(objectId `string`, [sessionToken `string`], callback `function`) + +If you are using the master key you don't need any session tokens. + +```javascript +app.deleteUser('someId', function (err, response) { + // response = {} +}); +``` + +If you're using the rest api key you will need a session token and will only be able to delete the user object of the matching user. + +```javascript +app.deleteUser('someId', 'sessionToken', function (error, response) { + // response = {} +}); +``` + +### delete all users + +* deleteAllUsers(callback `function`) + +This will only work when using the master key. + +```javascript +app.deleteAllUsers(function (err, response) { + // response = [{success: {}, success: {}, ... }] +}); +``` + +### reset a password + +* passwordReset(data `string`, callback `function`) + +```javascript +//email is built into Parse's special User class +app.passwordReset(email, function(err, response){ + console.log(response); +}); +``` + +### edit a user object + +* updateUser(objectId `string`, data `object`, [sessionToken `string`], callback `function`) + +With master key + +```javascript +app.updateUser('someId', {email: 'foo@example.com'}, function(err, response){ + console.log(response); +}); +``` + +or with rest api key + +```javascript +app.updateUser('someId', {email: 'foo@example.com'}, 'sesstionToken', function(err, response){ + console.log(response); +}); +``` + +### batch requests + +* batch(requests `array`, callback `function`) + +```javascript +var requests = [ + { + method: 'POST', + path: '/1/classes/Foo', + body: { + foo: 'bar1', + baz: 'qux1' + } + }, + { + method: 'POST', + path: '/1/classes/Foo', + body: { + foo: 'bar2', + baz: 'qux2' + } + } +]; +app.batch(requests, function (error, response) { + // response = [{success: {createdAt: '', objectId: ''}, {success: {...}}}] +}); +``` + +### insert installation data + +```javascript +//first arg is either 'ios' or 'android'. second arg is either the Apple deviceToken or the Android installationId. +app.insertInstallationData("ios", "0123456784abcdef0123456789abcdef0123456789abcdef0123456789abcdef", function(err, response){ + if (err) { + console.log(err); + } else { + console.log(response); + } +}); +``` + +### insert installation data with timeZone + +```javascript +//first arg is either 'ios' or 'android'. second arg is either the Apple deviceToken or the Android installationId. Third arg is the timezone string. +app.insertInstallationDataWithTimeZone("ios", "0123456784abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "EST", function(err, response){ + if (err) { + console.log(err); + } else { + console.log(response); + } +}); +``` + +### insert installation data with channels + +```javascript +//first arg is either 'ios' or 'android'. second arg is either the Apple deviceToken or the Android installationId. Third arg is the channels array. +arr = ["news", "sports"]; +app.insertInstallationDataWithChannels("ios", "0123456784abcdef0123456789abcdef0123456789abcdef0123456789abcdef", arr, function(err, response){ + if (err) { + console.log(err); + } else { + console.log(response); + } +}); +``` + +### insert installation data with timeZone and channels + +```javascript +//first arg is either 'ios' or 'android'. second arg is either the Apple deviceToken or the Android installationId. Third arg is the timezone string. 4th is the channels array. +arr = ["news", "sports"]; +app.insertInstallationDataWithTimeZoneAndChannels("ios", "0123456784abcdef0123456789abcdef0123456789abcdef0123456789abcdef", "EST", arr, function(err, response){ + if (err) { + console.log(err); + } else { + console.log(response); + } +}); +``` + +### create a role for a particular user + +```javascript +//create a data object that links the user object's objectId to the role + +var data = { + name: 'Administrator', + ACL: { + "*": { + "read": true + } + }, + roles: { + "__op": "AddRelation", + "objects": [ + { + "__type": "Pointer", + "className": "_Role", + "objectId": "" + } + ] + }, + users: { + "__op": "AddRelation", + "objects": [ + { + "__type": "Pointer", + "className": "_User", + "objectId": "" + } + ] + } +}; + + app.insertRole(data, function(err, resp){ + console.log(resp); + }); +``` + +### get a role + +```javascript +//pass the role object's objectId +app.getRole("", function(err, resp){ + console.log(resp); +}); +``` + +### update a role + +```javascript +//pass the objectId of the role, data contains the user's objectId + +var data = { + users: { + "__op": "RemoveRelation", + "objects": [ + { + "__type": "Pointer", + "className": "_User", + "objectId": "" + } + ] + } +}; + + app.updateRole("", data, function(err, resp){ + console.log(resp); + }); +``` + +### delete a role + +```javascript +//pass the objectId of the role +app.deleteRole("", function(err, resp){}); +``` + +### get all the roles + +```javascript +app.getRoles(function(err, resp){}); +``` + +### get a role against a cetain param + +```javascript +var params = { + where: { name: "Administrator" } +}; + + app.getRoles(params, function(err, resp){ + console.log(resp); + }); +``` + +### send a push notification + +```javascript +//The data param has to follow the data structure as described in the [Parse REST API](https://www.parse.com/docs/rest#push) +var notification = { + channels: [''], + data: { + alert: "sending too many push notifications is obnoxious" + } +}; +app.sendPush(notification, function(err, resp){ + console.log(resp); +}); +``` + +### usage of a sessionToken for all operations + +If you use node-parse-api in a node.js server environement a client might send a sessionToken with a request to your server. +You can pass that sessionToken as a constructor option to perform database operations on behalf of that user session. + +This allows e.g. to find objects of classes that are restricted to be read by only that user (or role). + +```javascript +var Parse = require('node-parse-api').Parse; + +// let's assume this is a sessionToken of a user who is member of the role "moderator" +var sessionToken = '3h3gaa32bdd3h3gaa323h3gaa32bddbdd'; + +var options = { + app_id:'...', + api_key:'...', + session_token: sessionToken // , master_key:'...' could be used too +} + +var app = new Parse(options); + +// let's assume Foo is a class with read permission for "moderator"-users only +app.find('Foo', {objectId: 'someId'}, function (err, response) { + console.log(response); +}); +``` + +### note on sending dates + +```javascript +//when inserting a data, you must use the Parse date object structure, i.e.: +{ + "__type": "Date", + "iso": new Date("", "", "").toJSON() +} +``` + +# License + +node-parse-api is available under the MIT license. -### update +Copyright © 2015 Mike Leveton and contributors - app.update('Foo', 'someId', { foo: 'fubar' }, function (err, response) { - console.log(response); - }); +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: -### delete +The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. - app.delete('Foo', 'someId', function (err) { - // nothing to see here - }); +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. diff --git a/test/Parse.test.js b/test/Parse.test.js index dd2bef0..0d53d64 100644 --- a/test/Parse.test.js +++ b/test/Parse.test.js @@ -1,3 +1,4 @@ +// this file runs tests against the master key var Parse = require('../index').Parse; // use environment variables APPLICATION_ID and MASTER_KEY to test against @@ -15,55 +16,282 @@ if (!application_id || !master_key) { // global objects to test against var parse = new Parse(application_id, master_key); var className = 'NodeParseApiTest'; -var object = { foo: Math.floor(Math.random() * 10000) }; // ERROR: if you change the type -var stub; - -exports.insert = function (assert) { - parse.insert(className, object, function (err, response) { - err && console.log(err); - assert.ok(response); - stub = response; - assert.done(); +var className2 = 'NodeParseApiRelationTest'; +var stub1; +var stub2; +var stub3; +var stubRelation; + +exports.insert = function (test) { + var data = { + foo: 'bar0', + baz: 'qux0', + quux: 'quuux' + }; + parse.insert(className, data, function (error, response) { + test.expect(1); + test.ok(!error, 'There shoudn\'t be an error object.'); + stub1 = response; + test.done(); + }); +}; + +exports.batchInsert = function (test) { + var batchRequests = [ + { + method: 'POST', + path: '/1/classes/' + className, + body: { + foo: 'bar1', + baz: 'qux1', + quux: 'quuux' + } + }, + { + method: 'POST', + path: '/1/classes/' + className, + body: { + foo: 'bar2', + baz: 'qux2', + quux: 'quuux' + } + } + ]; + parse.batch(batchRequests, function (error, response) { + test.expect(1); + test.ok(!error, 'There shoudn\'t be an error object.'); + stub2 = response[0].success; + stub3 = response[1].success; + test.done(); + }); +}; + +exports.find = function (test) { + parse.find(className, stub1.objectId, function (err, response) { + test.equal(stub1.objectId, response.objectId); + test.done(); }); }; -exports.find = function (assert) { - parse.find(className, stub.objectId, function (err, response) { - assert.equal(object.foo, response.foo); - assert.done(); +exports.findManyNoConstraints = function (test) { + parse.find(className, '', function (error, response) { + test.expect(3); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.ok(response.results.length === 3, 'There should be 3 objects in response.results.'); + test.equal(stub1.objectId, response.results[0].objectId, 'The first object should have the same objectId as the stub object.'); + test.done(); }); }; -exports['find many'] = function (assert) { - parse.find(className, stub, function (err, response) { - assert.equal(1, response.results.length); - assert.equal(stub.objectId, response.results[0].objectId); - assert.equal(stub.createdAt, response.results[0].createdAt); - assert.equal(object.foo, response.results[0].foo); - assert.done(); +// order limit skip keys include +exports.findManyWithConstraints = { + order: function (test) { + var query = { + order: '-foo' + }; + parse.find(className, query, function (error, response) { + test.expect(4); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.equal('bar0', response.results[2].foo, 'response.results[2].foo should be "bar0".'); + test.equal('bar1', response.results[1].foo, 'response.results[1].foo should be "bar1".'); + test.equal('bar2', response.results[0].foo, 'response.results[0].foo should be "bar2".'); + test.done(); + }); + }, + 'order keys skip': function (test) { + var query = { + order: 'foo', + keys: 'baz', + skip: 2 + }; + parse.find(className, query, function (error, response) { + test.ok(!error, 'There shoudn\'t be an error object.'); + test.equal('qux2', response.results[0].baz, 'response.results[0].baz should be "qux2".'); + test.done(); + }); + }, + 'order limit': function (test) { + var query = { + order: '-foo', + limit: 2 + }; + parse.find(className, query, function (error, response) { + test.expect(4); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.ok(response.results.length === 2, 'There should be 2 objects in response.results.'); + test.equal('bar1', response.results[1].foo, 'response.results[1].foo should be "bar1".'); + test.equal('qux1', response.results[1].baz, 'response.results[1].baz should be "qux1".'); + test.done(); + }); + } +}; + +exports.deprecatedFindMany = { + setUp: function (callback) { + this.query = { + quux: 'quuux' + }; + callback(); + }, + '3 arguments': function (test) { + parse.findMany(className, this.query, function (error, response) { + test.expect(2); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.ok(response.results.length === 3, 'There should be 3 objects in response.results.'); + test.done(); + }); + }, + '5 arguments': function (test) { + parse.findMany(className, this.query, 'foo', 2, function (error, response) { + test.expect(3); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.ok(response.results.length === 2, 'There should be 2 objects in response.results.'); + test.equal('bar0', response.results[0].foo, 'response.results[0].foo should be "bar0".'); + test.done(); + }); + }, + '6 arguments': function (test) { + parse.findMany(className, this.query, 'foo', 2, 1, function (error, response) { + test.expect(3); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.ok(response.results.length === 2, 'There should be 2 objects in response.results.'); + test.equal('bar1', response.results[0].foo, 'response.results[0].foo should be "bar1".'); + test.done(); + }); + }, + 'invalid number of arguments': function (test) { + test.expect(1); + test.throws(function () {parse.findMany('foo', 'bar', 'baz', 'qux');}); + test.done(); + } +}; + +exports.update = function (test) { + stub1.foo = 'bar00'; + parse.update(className, stub1.objectId, {foo: 'bar00'}, function (error, response) { + test.expect(4); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.ok(response); + parse.find(className, stub1.objectId, function (error, response) { + test.ok(!error, 'There shoudn\'t be an error object.'); + test.equal(stub1.foo, response.foo, 'response.foo should be "bar00".'); + test.done(); + }); }); }; -exports.update = function (assert) { - do { - var num = Math.floor(Math.random() * 10000); - } while (num == object.foo); - object.foo = num; - - parse.update(className, stub.objectId, object, function (err, response) { - err && console.log(err); - assert.ok(response); - exports.find(assert); // retest find on the updated object +exports.insertClass2 = function (test) { + parse.insert(className2, {foo: 'bar'}, function (error, response) { + test.expect(1); + test.ok(!error, 'There shoudn\'t be an error object.'); + stubRelation = response; + test.done(); }); }; -exports['delete'] = function (assert) { - parse['delete'](className, stub.objectId, function (err) { - err && console.log(err); - assert.ok(!err); - parse.find(className, stub.objectId, function (err, response) { - assert.equal(404, err.type); - assert.done(); +exports.addRelation = function (test) { + parse.addRelation("secondObject", className2, stubRelation.objectId, className, stub1.objectId, function (error, response) { + test.expect(3); + test.ok(!error, 'There shoudn\'t be an error object.'); + + var query = { + where: { + $relatedTo: { + object: { + __type: 'Pointer', + className: className2, + objectId: stubRelation.objectId + }, + key: 'secondObject' + } + } + }; + parse.find(className, query, function (error, response) { + test.ok(!error, 'There shoudn\'t be an error object.'); + test.equal(stub1.foo, response.results[0].foo, 'The response object should contain the related object.'); + test.done(); }); }); }; + +exports.deleteOne = function (test) { + parse.delete(className2, stubRelation.objectId, function (error, response) { + test.expect(3); + test.ok(!error, 'There shouldn\'t be an error object.'); + parse.find(className2, stubRelation.objectId, function (error, response) { + test.ok(error, 'There should be an error object.'); + test.equal(101, error.code, 'error.code should be 101.'); + test.done(); + }); + }); +}; + +exports.deleteAll = function (test) { + parse.deleteAll(className, function (error, response) { + test.expect(2); + test.ok(!error, 'There shoudn\'t be an error object.'); + test.ok(response[0].hasOwnProperty('success')); + test.done(); + }); +}; + +exports.installationTests = { + upsertInstallation: function(test) { + parse.upsertInstallation('ios', '0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', {userID: 'jenny'}, function(error, response) { + test.ok(!error, 'There shouldn\'t be an error object'); + test.done(); + }); + }, + + deleteInstallation: function(test) { + parse.getInstallationDataForDeviceToken('0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef', function(error, response) { + var id = response.results[0].objectId; + parse.deleteInstallation(id, function(error, response){ + test.ok(!error, 'There shouldn\'t be an error obejct'); + test.done(); + }); + }); + } +} + +exports.userTests = { + insertUser : function (test) { + test.expect(1); + parse.insertUser({username: 'foo', password: 'bar'}, function (error, response) { + test.ok(!error, 'There shoudn\'t be an error object.'); + test.done(); + }); + }, + getUser: function (test) { + test.expect(2); + parse.getUser({where:{username: 'foo'}}, function (error, response) { + test.ok(!error, 'There shoudn\'t be an error object.'); + test.equal('foo', response.results[0].username, 'response.results[0].username should be foo.'); + test.done(); + }); + }, + deleteUser: function (test) { + test.expect(1); + parse.getUser({where:{username: 'foo'}}, function (error, response) { + parse.deleteUser(response.results[0].objectId, function (error, response) { + test.ok(!error, 'There shoudn\'t be an error object.'); + test.done(); + }); + }); + } +}; + +exports.pushNotificationError = function (test) { + parse.sendPush({ + channels: ['foobar'], + data2: { + alert: 'test message' + } + }, function (error, response) { + test.expect(3); + test.ok(error); + test.equal(response, null); + test.equal(error.code, 115, 'error.code should be 115.'); + test.done(); + }); +}; diff --git a/test/testRestApiKey.js b/test/testRestApiKey.js new file mode 100644 index 0000000..01d95da --- /dev/null +++ b/test/testRestApiKey.js @@ -0,0 +1,78 @@ +// this file runs tests against the rest api key +var Parse = require('../index').Parse; + +// use environment variables APPLICATION_ID and MASTER_KEY to test against +var application_id = process.env.APPLICATION_ID; +var rest_api_key = process.env.REST_API_KEY; + +// require the environment variables, or exit with explanation +if (!application_id || !rest_api_key) { + console.log('Set the following environment variables for the test Parse app'); + console.log(' export APPLICATION_ID=...'); + console.log(' export REST_API_KEY=...'); + process.exit(1); +} + +// global objects to test against +var parse = new Parse({app_id: application_id, api_key: rest_api_key}); +var user = {username: 'foo', password: 'bar'}; +var user2 = {username: 'baz', password: 'qux'}; +var userObject; +var user2Object; + +exports.userLogin = function (test) { + test.expect(4); + parse.insertUser(user2, function (error, response) { + test.ok(!error, 'Failed creating user.'); + user2Object = response; + parse.insertUser(user, function (error, response) { + test.ok(!error, 'Failed creating user.'); + userObject = response; + parse.loginUser(user.username, user.password, function (error, response) { + test.ok(!error, 'Login failed.'); + test.equal(user.username, response.username, 'Should be the same username.'); + test.done(); + }); + }); + }); +}; + +exports.editUser = function (test) { + test.expect(3); + parse.updateUser(userObject.objectId, {username: 'foo0'}, userObject.sessionToken, function (error, response) { + userObject.username = 'foo0'; + test.ok(!error); + parse.getUser({objectId: userObject.objectId}, function (error, response) { + test.ok(!error); + test.equal(userObject.username, response.username, 'usernames should be the same.'); + test.done(); + }); + }); +}; + +exports.editUserFail = function (test) { + test.expect(1); + parse.updateUser(user2Object.objectId, {username: 'baz0'}, userObject.sessionToken, function (error, response) { + test.ok(error); + test.done(); + }); +}; + +exports.me = function (test) { + test.expect(1); + parse.me(userObject.sessionToken, function (error, response) { + test.ok(!error); + test.done(); + }); +}; + +exports.deleteUser = function (test) { + test.expect(2); + parse.deleteUser(userObject.objectId, userObject.sessionToken, function (error, response) { + test.ok(!error); + parse.deleteUser(user2Object.objectId, user2Object.sessionToken, function (error, response) { + test.ok(!error); + test.done(); + }); + }); +};