Merged with master

This commit is contained in:
Peter Theill
2016-02-09 10:23:55 +01:00
parent 307a5d8157
commit 7fa4b3bc07
57 changed files with 325 additions and 109 deletions

5
.babelrc Normal file
View File

@@ -0,0 +1,5 @@
{
"presets": [
"es2015"
]
}

3
.gitignore vendored
View File

@@ -31,3 +31,6 @@ node_modules
# WebStorm/IntelliJ # WebStorm/IntelliJ
.idea .idea
# Babel.js
lib/

View File

@@ -8,4 +8,7 @@ node_js:
env: env:
- MONGODB_VERSION=2.6.11 - MONGODB_VERSION=2.6.11
- MONGODB_VERSION=3.0.8 - MONGODB_VERSION=3.0.8
cache:
directories:
- $HOME/.mongodb/versions/downloads
after_success: ./node_modules/.bin/codecov after_success: ./node_modules/.bin/codecov

View File

@@ -7,7 +7,7 @@ We really want Parse to be yours, to see it grow and thrive in the open source c
##### Please Do's ##### Please Do's
* Take testing seriously! Aim to increase the test coverage with every pull request. * Take testing seriously! Aim to increase the test coverage with every pull request.
* Run the tests for the file you are working on with `TESTING=1 (repo-root)/node_modules/jasmine/bin/jasmine.js spec/MyFile.spec.js` * Run the tests for the file you are working on with `npm test spec/MyFile.spec.js`
* Run the tests for the whole project and look at the coverage report to make sure your tests are exhaustive by running `npm test` and looking at (project-root)/lcov-report/parse-server/FileUnderTest.js.html * Run the tests for the whole project and look at the coverage report to make sure your tests are exhaustive by running `npm test` and looking at (project-root)/lcov-report/parse-server/FileUnderTest.js.html
##### Code of Conduct ##### Code of Conduct

View File

@@ -1,6 +1,6 @@
#!/usr/bin/env node #!/usr/bin/env node
var express = require('express'); var express = require('express');
var ParseServer = require("../index").ParseServer; var ParseServer = require("../lib/index").ParseServer;
var app = express(); var app = express();

View File

@@ -2,7 +2,7 @@
"name": "parse-server", "name": "parse-server",
"version": "2.0.7", "version": "2.0.7",
"description": "An express module providing a Parse-compatible API server", "description": "An express module providing a Parse-compatible API server",
"main": "index.js", "main": "lib/index.js",
"repository": { "repository": {
"type": "git", "type": "git",
"url": "https://github.com/ParsePlatform/parse-server" "url": "https://github.com/ParsePlatform/parse-server"
@@ -11,6 +11,7 @@
"dependencies": { "dependencies": {
"apn": "^1.7.5", "apn": "^1.7.5",
"aws-sdk": "~2.2.33", "aws-sdk": "~2.2.33",
"babel-runtime": "^6.5.0",
"bcrypt-nodejs": "0.0.3", "bcrypt-nodejs": "0.0.3",
"body-parser": "^1.14.2", "body-parser": "^1.14.2",
"deepcopy": "^0.6.1", "deepcopy": "^0.6.1",
@@ -19,23 +20,29 @@
"mime": "^1.3.4", "mime": "^1.3.4",
"mongodb": "~2.1.0", "mongodb": "~2.1.0",
"multer": "^1.1.0", "multer": "^1.1.0",
"node-gcm": "^0.14.0",
"parse": "^1.7.0", "parse": "^1.7.0",
"randomstring": "^1.1.3", "randomstring": "^1.1.3",
"node-gcm": "^0.14.0",
"request": "^2.65.0" "request": "^2.65.0"
}, },
"devDependencies": { "devDependencies": {
"babel-cli": "^6.5.1",
"babel-core": "^6.5.1",
"babel-istanbul": "^0.6.0",
"babel-preset-es2015": "^6.5.0",
"babel-register": "^6.5.1",
"codecov": "^1.0.1", "codecov": "^1.0.1",
"deep-diff": "^0.3.3", "deep-diff": "^0.3.3",
"istanbul": "^0.4.2",
"jasmine": "^2.3.2", "jasmine": "^2.3.2",
"mongodb-runner": "^3.1.15" "mongodb-runner": "^3.1.15"
}, },
"scripts": { "scripts": {
"build": "./node_modules/.bin/babel src/ -d lib/",
"pretest": "MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} mongodb-runner start", "pretest": "MONGODB_VERSION=${MONGODB_VERSION:=3.0.8} mongodb-runner start",
"test": "NODE_ENV=test TESTING=1 ./node_modules/.bin/istanbul cover --include-all-sources -x **/spec/** ./node_modules/.bin/jasmine", "test": "NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/.bin/babel-istanbul cover -x **/spec/** ./node_modules/.bin/jasmine",
"posttest": "mongodb-runner stop", "posttest": "mongodb-runner stop",
"start": "./bin/parse-server" "start": "./bin/parse-server",
"prepublish": "npm run build"
}, },
"engines": { "engines": {
"node": ">=4.1" "node": ">=4.1"

View File

@@ -1,4 +1,4 @@
var APNS = require('../APNS'); var APNS = require('../src/APNS');
describe('APNS', () => { describe('APNS', () => {
it('can generate APNS notification', (done) => { it('can generate APNS notification', (done) => {

View File

@@ -1,4 +1,4 @@
var ExportAdapter = require('../ExportAdapter'); var ExportAdapter = require('../src/ExportAdapter');
describe('ExportAdapter', () => { describe('ExportAdapter', () => {
it('can be constructed', (done) => { it('can be constructed', (done) => {

View File

@@ -1,4 +1,4 @@
var GCM = require('../GCM'); var GCM = require('../src/GCM');
describe('GCM', () => { describe('GCM', () => {
it('can generate GCM Payload without expiration time', (done) => { it('can generate GCM Payload without expiration time', (done) => {

View File

@@ -251,6 +251,9 @@ describe('Parse.ACL', () => {
equal(results.length, 1); equal(results.length, 1);
var result = results[0]; var result = results[0];
ok(result); ok(result);
if (!result) {
return fail();
}
equal(result.id, object.id); equal(result.id, object.id);
equal(result.getACL().getReadAccess(user), true); equal(result.getACL().getReadAccess(user), true);
equal(result.getACL().getWriteAccess(user), true); equal(result.getACL().getWriteAccess(user), true);

View File

@@ -1,7 +1,7 @@
// A bunch of different tests are in here - it isn't very thematic. // A bunch of different tests are in here - it isn't very thematic.
// It would probably be better to refactor them into different files. // It would probably be better to refactor them into different files.
var DatabaseAdapter = require('../DatabaseAdapter'); var DatabaseAdapter = require('../src/DatabaseAdapter');
var request = require('request'); var request = require('request');
describe('miscellaneous', function() { describe('miscellaneous', function() {

View File

@@ -1,12 +1,12 @@
// These tests check the Installations functionality of the REST API. // These tests check the Installations functionality of the REST API.
// Ported from installation_collection_test.go // Ported from installation_collection_test.go
var auth = require('../Auth'); var auth = require('../src/Auth');
var cache = require('../cache'); var cache = require('../src/cache');
var Config = require('../Config'); var Config = require('../src/Config');
var DatabaseAdapter = require('../DatabaseAdapter'); var DatabaseAdapter = require('../src/DatabaseAdapter');
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
var rest = require('../rest'); var rest = require('../src/rest');
var config = new Config('test'); var config = new Config('test');
var database = DatabaseAdapter.getDatabaseConnection('test'); var database = DatabaseAdapter.getDatabaseConnection('test');

View File

@@ -2056,7 +2056,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it('query match on array value', (done) => { it('query match on array with single object', (done) => {
var target = {__type: 'Pointer', className: 'TestObject', objectId: 'abc123'}; var target = {__type: 'Pointer', className: 'TestObject', objectId: 'abc123'};
var obj = new Parse.Object('TestObject'); var obj = new Parse.Object('TestObject');
obj.set('someObjs', [target]); obj.set('someObjs', [target]);
@@ -2072,4 +2072,20 @@ describe('Parse.Query testing', () => {
}); });
}); });
it('query match on array with multiple objects', (done) => {
var target1 = {__type: 'Pointer', className: 'TestObject', objectId: 'abc'};
var target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'};
var obj= new Parse.Object('TestObject');
obj.set('someObjs', [target1, target2]);
obj.save().then(() => {
var query = new Parse.Query('TestObject');
query.equalTo('someObjs', target1);
return query.find();
}).then((results) => {
expect(results.length).toEqual(1);
done();
}, (error) => {
console.log(error);
});
});
}); });

View File

@@ -6,7 +6,7 @@
// Tests that involve sending password reset emails. // Tests that involve sending password reset emails.
var request = require('request'); var request = require('request');
var passwordCrypto = require('../password'); var passwordCrypto = require('../src/password');
describe('Parse.User testing', () => { describe('Parse.User testing', () => {
it("user sign up class method", (done) => { it("user sign up class method", (done) => {

View File

@@ -1,10 +1,10 @@
// These tests check the "create" functionality of the REST API. // These tests check the "create" functionality of the REST API.
var auth = require('../Auth'); var auth = require('../src/Auth');
var cache = require('../cache'); var cache = require('../src/cache');
var Config = require('../Config'); var Config = require('../src/Config');
var DatabaseAdapter = require('../DatabaseAdapter'); var DatabaseAdapter = require('../src/DatabaseAdapter');
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
var rest = require('../rest'); var rest = require('../src/rest');
var request = require('request'); var request = require('request');
var config = new Config('test'); var config = new Config('test');

View File

@@ -1,8 +1,8 @@
// These tests check the "find" functionality of the REST API. // These tests check the "find" functionality of the REST API.
var auth = require('../Auth'); var auth = require('../src/Auth');
var cache = require('../cache'); var cache = require('../src/cache');
var Config = require('../Config'); var Config = require('../src/Config');
var rest = require('../rest'); var rest = require('../src/rest');
var config = new Config('test'); var config = new Config('test');
var nobody = auth.nobody(config); var nobody = auth.nobody(config);

View File

@@ -1,6 +1,6 @@
// These tests check that the Schema operates correctly. // These tests check that the Schema operates correctly.
var Config = require('../Config'); var Config = require('../src/Config');
var Schema = require('../Schema'); var Schema = require('../src/Schema');
var dd = require('deep-diff'); var dd = require('deep-diff');
var config = new Config('test'); var config = new Config('test');
@@ -252,7 +252,7 @@ describe('Schema', () => {
it('refuses to add fields with invalid pointer types', done => { it('refuses to add fields with invalid pointer types', done => {
config.database.loadSchema() config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('NewClass', { .then(schema => schema.addClassIfNotExists('NewClass', {
foo: {type: 'Pointer'}, foo: {type: 'Pointer'}
})) }))
.catch(error => { .catch(error => {
expect(error.code).toEqual(135); expect(error.code).toEqual(135);
@@ -398,7 +398,7 @@ describe('Schema', () => {
config.database.loadSchema() config.database.loadSchema()
.then(schema => schema.addClassIfNotExists('NewClass', { .then(schema => schema.addClassIfNotExists('NewClass', {
geo1: {type: 'GeoPoint'}, geo1: {type: 'GeoPoint'},
geo2: {type: 'GeoPoint'}, geo2: {type: 'GeoPoint'}
})) }))
.catch(error => { .catch(error => {
expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE);

View File

@@ -2,11 +2,11 @@
jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000; jasmine.DEFAULT_TIMEOUT_INTERVAL = 2000;
var cache = require('../cache'); var cache = require('../src/cache');
var DatabaseAdapter = require('../DatabaseAdapter'); var DatabaseAdapter = require('../src/DatabaseAdapter');
var express = require('express'); var express = require('express');
var facebook = require('../facebook'); var facebook = require('../src/facebook');
var ParseServer = require('../index').ParseServer; var ParseServer = require('../src/index').ParseServer;
var databaseURI = process.env.DATABASE_URI; var databaseURI = process.env.DATABASE_URI;
var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js'; var cloudMain = process.env.CLOUD_CODE_MAIN || './cloud/main.js';

View File

@@ -1,4 +1,4 @@
var push = require('../push'); var push = require('../src/push');
describe('push', () => { describe('push', () => {
it('can check valid master key of request', (done) => { it('can check valid master key of request', (done) => {

View File

@@ -1,5 +1,7 @@
var Parse = require('parse/node').Parse;
var request = require('request'); var request = require('request');
var dd = require('deep-diff'); var dd = require('deep-diff');
var hasAllPODobject = () => { var hasAllPODobject = () => {
var obj = new Parse.Object('HasAllPOD'); var obj = new Parse.Object('HasAllPOD');
obj.set('aNumber', 5); obj.set('aNumber', 5);
@@ -14,9 +16,9 @@ var hasAllPODobject = () => {
objACL.setPublicWriteAccess(false); objACL.setPublicWriteAccess(false);
obj.setACL(objACL); obj.setACL(objACL);
return obj; return obj;
} };
var expectedResponseForHasAllPOD = { var plainOldDataSchema = {
className: 'HasAllPOD', className: 'HasAllPOD',
fields: { fields: {
//Default fields //Default fields
@@ -33,10 +35,10 @@ var expectedResponseForHasAllPOD = {
aArray: {type: 'Array'}, aArray: {type: 'Array'},
aGeoPoint: {type: 'GeoPoint'}, aGeoPoint: {type: 'GeoPoint'},
aFile: {type: 'File'} aFile: {type: 'File'}
}, }
}; };
var expectedResponseforHasPointersAndRelations = { var pointersAndRelationsSchema = {
className: 'HasPointersAndRelations', className: 'HasPointersAndRelations',
fields: { fields: {
//Default fields //Default fields
@@ -56,17 +58,30 @@ var expectedResponseforHasPointersAndRelations = {
}, },
} }
var noAuthHeaders = {
'X-Parse-Application-Id': 'test',
};
var restKeyHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
};
var masterKeyHeaders = {
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
};
describe('schemas', () => { describe('schemas', () => {
it('requires the master key to get all schemas', (done) => { it('requires the master key to get all schemas', (done) => {
request.get({ request.get({
url: 'http://localhost:8378/1/schemas', url: 'http://localhost:8378/1/schemas',
json: true, json: true,
headers: { headers: noAuthHeaders,
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(401); //api.parse.com uses status code 401, but due to the lack of keys
//being necessary in parse-server, 403 makes more sense
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized'); expect(body.error).toEqual('unauthorized');
done(); done();
}); });
@@ -76,10 +91,7 @@ describe('schemas', () => {
request.get({ request.get({
url: 'http://localhost:8378/1/schemas/SomeSchema', url: 'http://localhost:8378/1/schemas/SomeSchema',
json: true, json: true,
headers: { headers: restKeyHeaders,
'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest',
},
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(401); expect(response.statusCode).toEqual(401);
expect(body.error).toEqual('unauthorized'); expect(body.error).toEqual('unauthorized');
@@ -87,14 +99,23 @@ describe('schemas', () => {
}); });
}); });
it('asks for the master key if you use the rest key', (done) => {
request.get({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: restKeyHeaders,
}, (error, response, body) => {
expect(response.statusCode).toEqual(401);
expect(body.error).toEqual('master key not specified');
done();
});
});
it('responds with empty list when there are no schemas', done => { it('responds with empty list when there are no schemas', done => {
request.get({ request.get({
url: 'http://localhost:8378/1/schemas', url: 'http://localhost:8378/1/schemas',
json: true, json: true,
headers: { headers: masterKeyHeaders,
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => { }, (error, response, body) => {
expect(body.results).toEqual([]); expect(body.results).toEqual([]);
done(); done();
@@ -113,13 +134,10 @@ describe('schemas', () => {
request.get({ request.get({
url: 'http://localhost:8378/1/schemas', url: 'http://localhost:8378/1/schemas',
json: true, json: true,
headers: { headers: masterKeyHeaders,
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => { }, (error, response, body) => {
var expected = { var expected = {
results: [expectedResponseForHasAllPOD,expectedResponseforHasPointersAndRelations] results: [plainOldDataSchema,pointersAndRelationsSchema]
}; };
expect(body).toEqual(expected); expect(body).toEqual(expected);
done(); done();
@@ -133,12 +151,9 @@ describe('schemas', () => {
request.get({ request.get({
url: 'http://localhost:8378/1/schemas/HasAllPOD', url: 'http://localhost:8378/1/schemas/HasAllPOD',
json: true, json: true,
headers: { headers: masterKeyHeaders,
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => { }, (error, response, body) => {
expect(body).toEqual(expectedResponseForHasAllPOD); expect(body).toEqual(plainOldDataSchema);
done(); done();
}); });
}); });
@@ -150,10 +165,7 @@ describe('schemas', () => {
request.get({ request.get({
url: 'http://localhost:8378/1/schemas/HASALLPOD', url: 'http://localhost:8378/1/schemas/HASALLPOD',
json: true, json: true,
headers: { headers: masterKeyHeaders,
'X-Parse-Application-Id': 'test',
'X-Parse-Master-Key': 'test',
},
}, (error, response, body) => { }, (error, response, body) => {
expect(response.statusCode).toEqual(400); expect(response.statusCode).toEqual(400);
expect(body).toEqual({ expect(body).toEqual({
@@ -164,4 +176,146 @@ describe('schemas', () => {
}); });
}); });
}); });
it('requires the master key to create a schema', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: noAuthHeaders,
body: {
className: 'MyClass',
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(403);
expect(body.error).toEqual('unauthorized');
done();
});
});
it('asks for the master key if you use the rest key', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
json: true,
headers: restKeyHeaders,
body: {
className: 'MyClass',
},
}, (error, response, body) => {
expect(response.statusCode).toEqual(401);
expect(body.error).toEqual('master key not specified');
done();
});
});
it('sends an error if you use mismatching class names', done => {
request.post({
url: 'http://localhost:8378/1/schemas/A',
headers: masterKeyHeaders,
json: true,
body: {
className: 'B',
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body).toEqual({
code: Parse.Error.INVALID_CLASS_NAME,
error: 'class name mismatch between B and A',
});
done();
});
});
it('sends an error if you use no class name', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {},
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body).toEqual({
code: 135,
error: 'POST /schemas needs class name',
});
done();
})
});
it('sends an error if you try to create the same class twice', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {
className: 'A',
},
}, (error, response, body) => {
expect(error).toEqual(null);
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {
className: 'A',
}
}, (error, response, body) => {
expect(response.statusCode).toEqual(400);
expect(body).toEqual({
code: Parse.Error.INVALID_CLASS_NAME,
error: 'class A already exists',
});
done();
});
});
});
it('responds with all fields when you create a class', done => {
request.post({
url: 'http://localhost:8378/1/schemas',
headers: masterKeyHeaders,
json: true,
body: {
className: "NewClass",
fields: {
foo: {type: 'Number'},
ptr: {type: 'Pointer', targetClass: 'SomeClass'}
}
}
}, (error, response, body) => {
expect(body).toEqual({
className: 'NewClass',
fields: {
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
foo: {type: 'Number'},
ptr: {type: 'Pointer', targetClass: 'SomeClass'},
}
});
done();
});
});
it('lets you specify class name in both places', done => {
request.post({
url: 'http://localhost:8378/1/schemas/NewClass',
headers: masterKeyHeaders,
json: true,
body: {
className: "NewClass",
}
}, (error, response, body) => {
expect(body).toEqual({
className: 'NewClass',
fields: {
ACL: {type: 'ACL'},
createdAt: {type: 'Date'},
updatedAt: {type: 'Date'},
objectId: {type: 'String'},
}
});
done();
});
});
}); });

View File

@@ -4,7 +4,7 @@
"*spec.js" "*spec.js"
], ],
"helpers": [ "helpers": [
"../node_modules/babel-core/register.js",
"helper.js" "helper.js"
] ]
} }

View File

@@ -1,6 +1,6 @@
// These tests are unit tests designed to only test transform.js. // These tests are unit tests designed to only test transform.js.
var transform = require('../transform'); var transform = require('../src/transform');
var dummySchema = { var dummySchema = {
data: {}, data: {},

View File

@@ -60,7 +60,7 @@ APNS.prototype.send = function(data, deviceTokens) {
var generateNotification = function(coreData, expirationTime) { var generateNotification = function(coreData, expirationTime) {
var notification = new apn.notification(); var notification = new apn.notification();
var payload = {}; var payload = {};
for (key in coreData) { for (var key in coreData) {
switch (key) { switch (key) {
case 'alert': case 'alert':
notification.setAlertText(coreData.alert); notification.setAlertText(coreData.alert);

View File

@@ -34,21 +34,8 @@ ExportAdapter.prototype.connect = function() {
return this.connectionPromise; return this.connectionPromise;
} }
//http://regexr.com/3cncm
if (!this.mongoURI.match(/^mongodb:\/\/((.+):(.+)@)?([^:@]+):{0,1}([^:]+)\/(.+?)$/gm)) {
throw new Error("Invalid mongoURI: " + this.mongoURI)
}
var usernameStart = this.mongoURI.indexOf('://') + 3;
var lastAtIndex = this.mongoURI.lastIndexOf('@');
var encodedMongoURI = this.mongoURI;
var split = null;
if (lastAtIndex > 0) {
split = this.mongoURI.slice(usernameStart, lastAtIndex).split(':');
encodedMongoURI = this.mongoURI.slice(0, usernameStart) + encodeURIComponent(split[0]) + ':' + encodeURIComponent(split[1]) + this.mongoURI.slice(lastAtIndex);
}
this.connectionPromise = Promise.resolve().then(() => { this.connectionPromise = Promise.resolve().then(() => {
return MongoClient.connect(encodedMongoURI, {uri_decode_auth:true}); return MongoClient.connect(this.mongoURI);
}).then((db) => { }).then((db) => {
this.db = db; this.db = db;
}); });

View File

View File

@@ -17,7 +17,7 @@
var Parse = require('parse/node').Parse; var Parse = require('parse/node').Parse;
var transform = require('./transform'); var transform = require('./transform');
defaultColumns = { var defaultColumns = {
// Contain the default columns for every parse object type (except _Join collection) // Contain the default columns for every parse object type (except _Join collection)
_Default: { _Default: {
"objectId": {type:'String'}, "objectId": {type:'String'},
@@ -43,13 +43,13 @@ defaultColumns = {
"GCMSenderId": {type:'String'}, "GCMSenderId": {type:'String'},
"timeZone": {type:'String'}, "timeZone": {type:'String'},
"localeIdentifier": {type:'String'}, "localeIdentifier": {type:'String'},
"badge": {type:'Number'}, "badge": {type:'Number'}
}, },
// The additional default columns for the _User collection (in addition to DefaultCols) // The additional default columns for the _User collection (in addition to DefaultCols)
_Role: { _Role: {
"name": {type:'String'}, "name": {type:'String'},
"users": {type:'Relation',className:'_User'}, "users": {type:'Relation',className:'_User'},
"roles": {type:'Relation',className:'_Role'}, "roles": {type:'Relation',className:'_Role'}
}, },
// The additional default columns for the _User collection (in addition to DefaultCols) // The additional default columns for the _User collection (in addition to DefaultCols)
_Session: { _Session: {
@@ -58,12 +58,9 @@ defaultColumns = {
"installationId": {type:'String'}, "installationId": {type:'String'},
"sessionToken": {type:'String'}, "sessionToken": {type:'String'},
"expiresAt": {type:'Date'}, "expiresAt": {type:'Date'},
"createdWith": {type:'Object'}, "createdWith": {type:'Object'}
}, }
_GlobalConfig: { };
"params": {type:'Object'}
},
}
// Valid classes must: // Valid classes must:
// Be one of _User, _Installation, _Role, _Session OR // Be one of _User, _Installation, _Role, _Session OR
@@ -224,7 +221,7 @@ Schema.prototype.addClassIfNotExists = function(className, fields) {
error: invalidClassNameMessage(className), error: invalidClassNameMessage(className),
}); });
} }
for (fieldName in fields) { for (var fieldName in fields) {
if (!fieldNameIsValid(fieldName)) { if (!fieldNameIsValid(fieldName)) {
return Promise.reject({ return Promise.reject({
code: Parse.Error.INVALID_KEY_NAME, code: Parse.Error.INVALID_KEY_NAME,
@@ -243,18 +240,18 @@ Schema.prototype.addClassIfNotExists = function(className, fields) {
_id: className, _id: className,
objectId: 'string', objectId: 'string',
updatedAt: 'string', updatedAt: 'string',
createdAt: 'string', createdAt: 'string'
}; };
for (fieldName in defaultColumns[className]) { for (var fieldName in defaultColumns[className]) {
validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]); var validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]);
if (validatedField.code) { if (validatedField.code) {
return Promise.reject(validatedField); return Promise.reject(validatedField);
} }
mongoObject[fieldName] = validatedField.result; mongoObject[fieldName] = validatedField.result;
} }
for (fieldName in fields) { for (var fieldName in fields) {
validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]); var validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]);
if (validatedField.code) { if (validatedField.code) {
return Promise.reject(validatedField); return Promise.reject(validatedField);
} }
@@ -262,7 +259,6 @@ Schema.prototype.addClassIfNotExists = function(className, fields) {
} }
var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint'); var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
if (geoPoints.length > 1) { if (geoPoints.length > 1) {
return Promise.reject({ return Promise.reject({
code: Parse.Error.INCORRECT_TYPE, code: Parse.Error.INCORRECT_TYPE,
@@ -281,7 +277,7 @@ Schema.prototype.addClassIfNotExists = function(className, fields) {
} }
return Promise.reject(error); return Promise.reject(error);
}); });
} };
// Returns a promise that resolves successfully to the new schema // Returns a promise that resolves successfully to the new schema
// object or fails with a reason. // object or fails with a reason.

View File

@@ -42,7 +42,7 @@ function handleFind(req) {
req.params.className, body.where, options) req.params.className, body.where, options)
.then((response) => { .then((response) => {
if (response && response.results) { if (response && response.results) {
for (result of response.results) { for (var result of response.results) {
if (result.sessionToken) { if (result.sessionToken) {
result.sessionToken = req.info.sessionToken || result.sessionToken; result.sessionToken = req.info.sessionToken || result.sessionToken;
} }

View File

@@ -1,7 +1,9 @@
// schemas.js // schemas.js
var express = require('express'), var express = require('express'),
PromiseRouter = require('./PromiseRouter'); Parse = require('parse/node').Parse,
PromiseRouter = require('./PromiseRouter'),
Schema = require('./Schema');
var router = new PromiseRouter(); var router = new PromiseRouter();
@@ -23,6 +25,7 @@ function mongoFieldTypeToSchemaAPIType(type) {
case 'string': return {type: 'String'}; case 'string': return {type: 'String'};
case 'boolean': return {type: 'Boolean'}; case 'boolean': return {type: 'Boolean'};
case 'date': return {type: 'Date'}; case 'date': return {type: 'Date'};
case 'map':
case 'object': return {type: 'Object'}; case 'object': return {type: 'Object'};
case 'array': return {type: 'Array'}; case 'array': return {type: 'Array'};
case 'geopoint': return {type: 'GeoPoint'}; case 'geopoint': return {type: 'GeoPoint'};
@@ -31,8 +34,8 @@ function mongoFieldTypeToSchemaAPIType(type) {
} }
function mongoSchemaAPIResponseFields(schema) { function mongoSchemaAPIResponseFields(schema) {
fieldNames = Object.keys(schema).filter(key => key !== '_id'); var fieldNames = Object.keys(schema).filter(key => key !== '_id' && key !== '_metadata');
response = fieldNames.reduce((obj, fieldName) => { var response = fieldNames.reduce((obj, fieldName) => {
obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName]) obj[fieldName] = mongoFieldTypeToSchemaAPIType(schema[fieldName])
return obj; return obj;
}, {}); }, {});
@@ -54,7 +57,7 @@ function getAllSchemas(req) {
if (!req.auth.isMaster) { if (!req.auth.isMaster) {
return Promise.resolve({ return Promise.resolve({
status: 401, status: 401,
response: {error: 'unauthorized'}, response: {error: 'master key not specified'},
}); });
} }
return req.config.database.collection('_SCHEMA') return req.config.database.collection('_SCHEMA')
@@ -83,7 +86,46 @@ function getOneSchema(req) {
})); }));
} }
function createSchema(req) {
if (!req.auth.isMaster) {
return Promise.resolve({
status: 401,
response: {error: 'master key not specified'},
});
}
if (req.params.className && req.body.className) {
if (req.params.className != req.body.className) {
return Promise.resolve({
status: 400,
response: {
code: Parse.Error.INVALID_CLASS_NAME,
error: 'class name mismatch between ' + req.body.className + ' and ' + req.params.className,
},
});
}
}
var className = req.params.className || req.body.className;
if (!className) {
return Promise.resolve({
status: 400,
response: {
code: 135,
error: 'POST ' + req.path + ' needs class name',
},
});
}
return req.config.database.loadSchema()
.then(schema => schema.addClassIfNotExists(className, req.body.fields))
.then(result => ({ response: mongoSchemaToSchemaAPIResponse(result) }))
.catch(error => ({
status: 400,
response: error,
}));
}
router.route('GET', '/schemas', getAllSchemas); router.route('GET', '/schemas', getAllSchemas);
router.route('GET', '/schemas/:className', getOneSchema); router.route('GET', '/schemas/:className', getOneSchema);
router.route('POST', '/schemas', createSchema);
router.route('POST', '/schemas/:className', createSchema);
module.exports = router; module.exports = router;

View File

@@ -21,7 +21,7 @@ var Parse = require('parse/node').Parse;
// validate: true indicates that key names are to be validated. // validate: true indicates that key names are to be validated.
// //
// Returns an object with {key: key, value: value}. // Returns an object with {key: key, value: value}.
function transformKeyValue(schema, className, restKey, restValue, options) { export function transformKeyValue(schema, className, restKey, restValue, options) {
options = options || {}; options = options || {};
// Check if the schema is known since it's a built-in field. // Check if the schema is known since it's a built-in field.
@@ -126,7 +126,7 @@ function transformKeyValue(schema, className, restKey, restValue, options) {
if (inArray && options.query && !(restValue instanceof Array)) { if (inArray && options.query && !(restValue instanceof Array)) {
return { return {
key: key, value: [restValue] key: key, value: { '$all' : [restValue] }
}; };
} }