Skip to content

Commit 5b960d3

Browse files
Merge remote-tracking branch 'upstream/master'
2 parents 4ddedf8 + 39fb0d0 commit 5b960d3

30 files changed

+689
-202
lines changed

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,3 +28,6 @@ node_modules
2828

2929
# Emacs
3030
*~
31+
32+
# WebStorm/IntelliJ
33+
.idea

.travis.yml

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
branches:
2+
only:
3+
- master
4+
language: node_js
5+
node_js:
6+
- "4.1"
7+
- "4.2"
8+
env:
9+
- MONGODB_VERSION=2.6.11
10+
- MONGODB_VERSION=3.0.8
11+
after_success: ./node_modules/.bin/codecov

Auth.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ var getAuthForSessionToken = function(config, sessionToken) {
6464
var obj = results[0]['user'];
6565
delete obj.password;
6666
obj['className'] = '_User';
67+
obj['sessionToken'] = sessionToken;
6768
var userObject = Parse.Object.fromJSON(obj);
6869
cache.setUser(sessionToken, userObject);
6970
return new Auth(config, false, userObject);

CONTRIBUTING.md

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
### Contributing to Parse Server
2+
3+
#### Pull Requests Welcome!
4+
5+
We really want Parse to be yours, to see it grow and thrive in the open source community.
6+
7+
##### Please Do's
8+
9+
* Please write tests to cover new methods.
10+
* Please run the tests and make sure you didn't break anything.
11+
12+
##### Code of Conduct
13+
14+
This project adheres to the [Open Code of Conduct][code-of-conduct]. By participating, you are expected to honor this code.
15+
[code-of-conduct]: http://todogroup.org/opencodeofconduct/#Parse Server/[email protected]
16+
17+

DatabaseAdapter.js

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ var adapter = ExportAdapter;
2020
var cache = require('./cache');
2121
var dbConnections = {};
2222
var databaseURI = 'mongodb://localhost:27017/parse';
23+
var appDatabaseURIs = {};
2324

2425
function setAdapter(databaseAdapter) {
2526
adapter = databaseAdapter;
@@ -29,11 +30,17 @@ function setDatabaseURI(uri) {
2930
databaseURI = uri;
3031
}
3132

33+
function setAppDatabaseURI(appId, uri) {
34+
appDatabaseURIs[appId] = uri;
35+
}
36+
3237
function getDatabaseConnection(appId) {
3338
if (dbConnections[appId]) {
3439
return dbConnections[appId];
3540
}
36-
dbConnections[appId] = new adapter(databaseURI, {
41+
42+
var dbURI = (appDatabaseURIs[appId] ? appDatabaseURIs[appId] : databaseURI);
43+
dbConnections[appId] = new adapter(dbURI, {
3744
collectionPrefix: cache.apps[appId]['collectionPrefix']
3845
});
3946
dbConnections[appId].connect();
@@ -44,5 +51,6 @@ module.exports = {
4451
dbConnections: dbConnections,
4552
getDatabaseConnection: getDatabaseConnection,
4653
setAdapter: setAdapter,
47-
setDatabaseURI: setDatabaseURI
54+
setDatabaseURI: setDatabaseURI,
55+
setAppDatabaseURI: setAppDatabaseURI
4856
};

ExportAdapter.js

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -34,8 +34,21 @@ ExportAdapter.prototype.connect = function() {
3434
return this.connectionPromise;
3535
}
3636

37+
//http://regexr.com/3cncm
38+
if (!this.mongoURI.match(/^mongodb:\/\/((.+):(.+)@)?([^:@]+):{0,1}([^:]+)\/(.+?)$/gm)) {
39+
throw new Error("Invalid mongoURI: " + this.mongoURI)
40+
}
41+
var usernameStart = this.mongoURI.indexOf('://') + 3;
42+
var lastAtIndex = this.mongoURI.lastIndexOf('@');
43+
var encodedMongoURI = this.mongoURI;
44+
var split = null;
45+
if (lastAtIndex > 0) {
46+
split = this.mongoURI.slice(usernameStart, lastAtIndex).split(':');
47+
encodedMongoURI = this.mongoURI.slice(0, usernameStart) + encodeURIComponent(split[0]) + ':' + encodeURIComponent(split[1]) + this.mongoURI.slice(lastAtIndex);
48+
}
49+
3750
this.connectionPromise = Promise.resolve().then(() => {
38-
return MongoClient.connect(this.mongoURI);
51+
return MongoClient.connect(encodedMongoURI, {uri_decode_auth:true});
3952
}).then((db) => {
4053
this.db = db;
4154
});
@@ -232,7 +245,7 @@ ExportAdapter.prototype.handleRelationUpdates = function(className,
232245
}
233246

234247
if (op.__op == 'Batch') {
235-
for (x of op.ops) {
248+
for (var x of op.ops) {
236249
process(x, key);
237250
}
238251
}

FilesAdapter.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@
55
// Adapter classes must implement the following functions:
66
// * create(config, filename, data)
77
// * get(config, filename)
8+
// * location(config, req, filename)
89
//
910
// Default is GridStoreAdapter, which requires mongo
1011
// and for the API server to be using the ExportAdapter

GridStoreAdapter.js

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
// Requires the database adapter to be based on mongoclient
55

66
var GridStore = require('mongodb').GridStore;
7+
var path = require('path');
78

89
// For a given config object, filename, and data, store a file
910
// Returns a promise
@@ -32,7 +33,16 @@ function get(config, filename) {
3233
});
3334
}
3435

36+
// Generates and returns the location of a file stored in GridStore for the
37+
// given request and filename
38+
function location(config, req, filename) {
39+
return (req.protocol + '://' + req.get('host') +
40+
path.dirname(req.originalUrl) + '/' + req.config.applicationId +
41+
'/' + encodeURIComponent(filename));
42+
}
43+
3544
module.exports = {
3645
create: create,
37-
get: get
46+
get: get,
47+
location: location
3848
};

README.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,9 @@
11
## parse-server
22

3+
[![Build Status](https://img.shields.io/travis/ParsePlatform/parse-server/master.svg?style=flat)](https://travis-ci.org/ParsePlatform/parse-server)
4+
[![Coverage Status](https://img.shields.io/codecov/c/github/ParsePlatform/parse-server/master.svg)](https://codecov.io/github/ParsePlatform/parse-server?branch=master)
5+
[![npm version](https://img.shields.io/npm/v/parse-server.svg?style=flat)](https://www.npmjs.com/package/parse-server)
6+
37
A Parse.com API compatible router package for Express
48

59
Read the announcement blog post here: http://blog.parse.com/announcements/introducing-parse-server-and-the-database-migration-tool/

RestQuery.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -434,7 +434,7 @@ function includePath(config, auth, response, path) {
434434
function findPointers(object, path) {
435435
if (object instanceof Array) {
436436
var answer = [];
437-
for (x of object) {
437+
for (var x of object) {
438438
answer = answer.concat(findPointers(x, path));
439439
}
440440
return answer;

RestWrite.js

Lines changed: 28 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -2,13 +2,14 @@
22
// that writes to the database.
33
// This could be either a "create" or an "update".
44

5+
var crypto = require('crypto');
56
var deepcopy = require('deepcopy');
67
var rack = require('hat').rack();
78

89
var Auth = require('./Auth');
910
var cache = require('./cache');
1011
var Config = require('./Config');
11-
var crypto = require('./crypto');
12+
var passwordCrypto = require('./password');
1213
var facebook = require('./facebook');
1314
var Parse = require('parse/node');
1415
var triggers = require('./triggers');
@@ -228,6 +229,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
228229
this.className,
229230
{'authData.facebook.id': facebookData.id}, {});
230231
}).then((results) => {
232+
this.storage['authProvider'] = "facebook";
231233
if (results.length > 0) {
232234
if (!this.query) {
233235
// We're signing up, but this user already exists. Short-circuit
@@ -236,6 +238,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
236238
response: results[0],
237239
location: this.location()
238240
};
241+
this.data.objectId = results[0].objectId;
239242
return;
240243
}
241244

@@ -248,6 +251,8 @@ RestWrite.prototype.handleFacebookAuthData = function() {
248251
// We're trying to create a duplicate FB auth. Forbid it
249252
throw new Parse.Error(Parse.Error.ACCOUNT_ALREADY_LINKED,
250253
'this auth is already used');
254+
} else {
255+
this.data.username = rack();
251256
}
252257

253258
// This FB auth does not already exist, so transform it to a
@@ -261,7 +266,7 @@ RestWrite.prototype.handleFacebookAuthData = function() {
261266

262267
// The non-third-party parts of User transformation
263268
RestWrite.prototype.transformUser = function() {
264-
if (this.response || this.className !== '_User') {
269+
if (this.className !== '_User') {
265270
return;
266271
}
267272

@@ -271,7 +276,8 @@ RestWrite.prototype.transformUser = function() {
271276
var token = 'r:' + rack();
272277
this.storage['token'] = token;
273278
promise = promise.then(() => {
274-
// TODO: Proper createdWith options, pass installationId
279+
var expiresAt = new Date();
280+
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
275281
var sessionData = {
276282
sessionToken: token,
277283
user: {
@@ -281,10 +287,15 @@ RestWrite.prototype.transformUser = function() {
281287
},
282288
createdWith: {
283289
'action': 'login',
284-
'authProvider': 'password'
290+
'authProvider': this.storage['authProvider'] || 'password'
285291
},
286-
restricted: false
292+
restricted: false,
293+
installationId: this.data.installationId,
294+
expiresAt: Parse._encode(expiresAt)
287295
};
296+
if (this.response && this.response.response) {
297+
this.response.response.sessionToken = token;
298+
}
288299
var create = new RestWrite(this.config, Auth.master(this.config),
289300
'_Session', null, sessionData);
290301
return create.execute();
@@ -299,7 +310,7 @@ RestWrite.prototype.transformUser = function() {
299310
if (this.query) {
300311
this.storage['clearSessions'] = true;
301312
}
302-
return crypto.hash(this.data.password).then((hashedPassword) => {
313+
return passwordCrypto.hash(this.data.password).then((hashedPassword) => {
303314
this.data._hashed_password = hashedPassword;
304315
delete this.data.password;
305316
});
@@ -361,7 +372,7 @@ RestWrite.prototype.handleFollowup = function() {
361372
};
362373
delete this.storage['clearSessions'];
363374
return this.config.database.destroy('_Session', sessionQuery)
364-
.then(this.handleFollowup);
375+
.then(this.handleFollowup.bind(this));
365376
}
366377
};
367378

@@ -403,6 +414,8 @@ RestWrite.prototype.handleSession = function() {
403414

404415
if (!this.query && !this.auth.isMaster) {
405416
var token = 'r:' + rack();
417+
var expiresAt = new Date();
418+
expiresAt.setFullYear(expiresAt.getFullYear() + 1);
406419
var sessionData = {
407420
sessionToken: token,
408421
user: {
@@ -414,7 +427,7 @@ RestWrite.prototype.handleSession = function() {
414427
'action': 'create'
415428
},
416429
restricted: true,
417-
expiresAt: 0
430+
expiresAt: Parse._encode(expiresAt)
418431
};
419432
for (var key in this.data) {
420433
if (key == 'objectId') {
@@ -701,15 +714,18 @@ RestWrite.prototype.objectId = function() {
701714
return this.data.objectId || this.query.objectId;
702715
};
703716

704-
// Returns a string that's usable as an object id.
705-
// Probably unique. Good enough? Probably!
717+
// Returns a unique string that's usable as an object id.
706718
function newObjectId() {
707719
var chars = ('ABCDEFGHIJKLMNOPQRSTUVWXYZ' +
708720
'abcdefghijklmnopqrstuvwxyz' +
709721
'0123456789');
710722
var objectId = '';
711-
for (var i = 0; i < 10; ++i) {
712-
objectId += chars[Math.floor(Math.random() * chars.length)];
723+
var bytes = crypto.randomBytes(10);
724+
for (var i = 0; i < bytes.length; ++i) {
725+
// Note: there is a slight modulo bias, because chars length
726+
// of 62 doesn't divide the number of all bytes (256) evenly.
727+
// It is acceptable for our purposes.
728+
objectId += chars[bytes.readUInt8(i) % chars.length];
713729
}
714730
return objectId;
715731
}

S3Adapter.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
// S3Adapter
2+
//
3+
// Stores Parse files in AWS S3.
4+
5+
var AWS = require('aws-sdk');
6+
var path = require('path');
7+
8+
var DEFAULT_REGION = "us-east-1";
9+
var DEFAULT_BUCKET = "parse-files";
10+
11+
// Creates an S3 session.
12+
// Providing AWS access and secret keys is mandatory
13+
// Region and bucket will use sane defaults if omitted
14+
function S3Adapter(accessKey, secretKey, options) {
15+
options = options || {};
16+
17+
this.region = options.region || DEFAULT_REGION;
18+
this.bucket = options.bucket || DEFAULT_BUCKET;
19+
this.bucketPrefix = options.bucketPrefix || "";
20+
this.directAccess = options.directAccess || false;
21+
22+
s3Options = {
23+
accessKeyId: accessKey,
24+
secretAccessKey: secretKey,
25+
params: {Bucket: this.bucket}
26+
};
27+
AWS.config.region = this.region;
28+
this.s3 = new AWS.S3(s3Options);
29+
}
30+
31+
// For a given config object, filename, and data, store a file in S3
32+
// Returns a promise containing the S3 object creation response
33+
S3Adapter.prototype.create = function(config, filename, data) {
34+
var params = {
35+
Key: this.bucketPrefix + filename,
36+
Body: data,
37+
};
38+
if (this.directAccess) {
39+
params.ACL = "public-read"
40+
}
41+
42+
return new Promise((resolve, reject) => {
43+
this.s3.upload(params, (err, data) => {
44+
if (err !== null) return reject(err);
45+
resolve(data);
46+
});
47+
});
48+
}
49+
50+
// Search for and return a file if found by filename
51+
// Returns a promise that succeeds with the buffer result from S3
52+
S3Adapter.prototype.get = function(config, filename) {
53+
var params = {Key: this.bucketPrefix + filename};
54+
55+
return new Promise((resolve, reject) => {
56+
this.s3.getObject(params, (err, data) => {
57+
if (err !== null) return reject(err);
58+
resolve(data.Body);
59+
});
60+
});
61+
}
62+
63+
// Generates and returns the location of a file stored in S3 for the given request and
64+
// filename
65+
// The location is the direct S3 link if the option is set, otherwise we serve
66+
// the file through parse-server
67+
S3Adapter.prototype.location = function(config, req, filename) {
68+
if (this.directAccess) {
69+
return ('https://' + this.bucket + '.s3.amazonaws.com' + '/' +
70+
this.bucketPrefix + filename);
71+
}
72+
return (req.protocol + '://' + req.get('host') +
73+
path.dirname(req.originalUrl) + '/' + req.config.applicationId +
74+
'/' + encodeURIComponent(filename));
75+
}
76+
77+
module.exports = S3Adapter;

Schema.js

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -212,6 +212,9 @@ Schema.prototype.validateObject = function(className, object) {
212212
var geocount = 0;
213213
var promise = this.validateClassName(className);
214214
for (var key in object) {
215+
if (object[key] === undefined) {
216+
continue;
217+
}
215218
var expected = getType(object[key]);
216219
if (expected === 'geopoint') {
217220
geocount++;

0 commit comments

Comments
 (0)