Postgres: $all, $and CLP and more (#2551)

* Adds passing tests

* Better containsAll implementation

* Full Geopoint support, fix inverted lat/lng

* Adds support for $and operator / PointerPermissions specs

* Fix issue updating CLPs on schema

* Extends query support

* Adds RestCreate to the specs

* Adds User specs

* Adds error handlers for failing tests

* nits

* Proper JSON update of AuthData

* fix for #1259 with PG

* Fix for Installations _PushStatus test

* Adds support for GlobalConfig

* Enables relations tests

* Exclude spec as legacy

* Makes corner case for 1 in GlobalConfig
This commit is contained in:
Florent Vilmart
2016-08-20 16:07:48 -04:00
committed by GitHub
parent e1de9f3a12
commit 9ab488b6a0
17 changed files with 276 additions and 138 deletions

View File

@@ -75,8 +75,7 @@ describe('InstallationsRouter', () => {
expect(results.length).toEqual(1); expect(results.length).toEqual(1);
done(); done();
}).catch((err) => { }).catch((err) => {
console.error(err); jfail(err);
fail(JSON.stringify(err));
done(); done();
}); });
}); });

View File

@@ -113,7 +113,7 @@ describe('miscellaneous', function() {
.catch(done); .catch(done);
}); });
it_exclude_dbs(['postgres'])('ensure that email is uniquely indexed', done => { it('ensure that email is uniquely indexed', done => {
let numFailed = 0; let numFailed = 0;
let numCreated = 0; let numCreated = 0;
let user1 = new Parse.User(); let user1 = new Parse.User();
@@ -212,7 +212,7 @@ describe('miscellaneous', function() {
}); });
}); });
it_exclude_dbs(['postgres'])('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => { it('ensure that if you try to sign up a user with a unique username and email, but duplicates in some other field that has a uniqueness constraint, you get a regular duplicate value error', done => {
let config = new Config('test'); let config = new Config('test');
config.database.adapter.addFieldIfNotExists('_User', 'randomField', { type: 'String' }) config.database.adapter.addFieldIfNotExists('_User', 'randomField', { type: 'String' })
.then(() => config.database.adapter.ensureUniqueness('_User', userSchema, ['randomField'])) .then(() => config.database.adapter.ensureUniqueness('_User', userSchema, ['randomField']))
@@ -233,7 +233,6 @@ it_exclude_dbs(['postgres'])('ensure that if you try to sign up a user with a un
return user.signUp() return user.signUp()
}) })
.catch(error => { .catch(error => {
console.error(error);
expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE);
done(); done();
}); });
@@ -1363,7 +1362,7 @@ it_exclude_dbs(['postgres'])('ensure that if you try to sign up a user with a un
}); });
}); });
it_exclude_dbs(['postgres'])('does not change inner objects if the key has the same name as a geopoint field on the class, and the value is an array of length 2, or if the key has the same name as a file field on the class, and the value is a string', done => { it('does not change inner objects if the key has the same name as a geopoint field on the class, and the value is an array of length 2, or if the key has the same name as a file field on the class, and the value is a string', done => {
let file = new Parse.File('myfile.txt', { base64: 'eAo=' }); let file = new Parse.File('myfile.txt', { base64: 'eAo=' });
file.save() file.save()
.then(f => { .then(f => {
@@ -1495,8 +1494,10 @@ it_exclude_dbs(['postgres'])('ensure that if you try to sign up a user with a un
done(); done();
}); });
}); });
});
it_exclude_dbs(['postgres'])('should have _acl when locking down (regression for #2465)', (done) =>  { describe_only_db('mongo')('legacy _acl', () => {
it('should have _acl when locking down (regression for #2465)', (done) =>  {
let headers = { let headers = {
'X-Parse-Application-Id': 'test', 'X-Parse-Application-Id': 'test',
'X-Parse-REST-API-Key': 'rest' 'X-Parse-REST-API-Key': 'rest'

View File

@@ -273,7 +273,7 @@ describe('Parse.GeoPoint testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('works with geobox queries', (done) => { it('works with geobox queries', (done) => {
var inSF = new Parse.GeoPoint(37.75, -122.4); var inSF = new Parse.GeoPoint(37.75, -122.4);
var southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398); var southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398);
var northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962); var northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962);

View File

@@ -7,15 +7,24 @@ let Config = require('../src/Config');
describe('a GlobalConfig', () => { describe('a GlobalConfig', () => {
beforeEach(done => { beforeEach(done => {
let config = new Config('test'); let config = new Config('test');
let query = on_db('mongo', () => {
// Legacy is with an int...
return { objectId: 1 };
}, () => {
return { objectId: "1" }
})
config.database.adapter.upsertOneObject( config.database.adapter.upsertOneObject(
'_GlobalConfig', '_GlobalConfig',
{ fields: {} }, { fields: { objectId: { type: 'Number' }, params: {type: 'Object'}} },
{ objectId: 1 }, query,
{ params: { companies: ['US', 'DK'] } } { params: { companies: ['US', 'DK'] } }
).then(done, done); ).then(done, (err) => {
jfail(err);
done();
});
}); });
it_exclude_dbs(['postgres'])('can be retrieved', (done) => { it('can be retrieved', (done) => {
request.get({ request.get({
url : 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json : true, json : true,
@@ -32,7 +41,7 @@ describe('a GlobalConfig', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('can be updated when a master key exists', (done) => { it('can be updated when a master key exists', (done) => {
request.put({ request.put({
url : 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json : true, json : true,
@@ -48,7 +57,7 @@ describe('a GlobalConfig', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('properly handles delete op', (done) => { it('properly handles delete op', (done) => {
request.put({ request.put({
url : 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json : true, json : true,
@@ -79,7 +88,7 @@ describe('a GlobalConfig', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('fail to update if master key is missing', (done) => { it('fail to update if master key is missing', (done) => {
request.put({ request.put({
url : 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',
json : true, json : true,
@@ -95,12 +104,12 @@ describe('a GlobalConfig', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('failed getting config when it is missing', (done) => { it('failed getting config when it is missing', (done) => {
let config = new Config('test'); let config = new Config('test');
config.database.adapter.deleteObjectsByQuery( config.database.adapter.deleteObjectsByQuery(
'_GlobalConfig', '_GlobalConfig',
{ fields: { params: { __type: 'String' } } }, { fields: { params: { __type: 'String' } } },
{ objectId: 1 } { objectId: "1" }
).then(() => { ).then(() => {
request.get({ request.get({
url : 'http://localhost:8378/1/config', url : 'http://localhost:8378/1/config',

View File

@@ -665,13 +665,7 @@ describe('Parse.Object testing', () => {
expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]); expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]);
done(); done();
}, (error) => { }, (error) => {
console.error(error);
on_db('mongo', () => {
jfail(error); jfail(error);
});
on_db('postgres', () => {
expect(error.message).toEqual("Postgres does not support Remove operator.");
});
done(); done();
}); });
}); });

View File

@@ -233,7 +233,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])("containsAll date array queries", function(done) { it("containsAll date array queries", function(done) {
var DateSet = Parse.Object.extend({ className: "DateSet" }); var DateSet = Parse.Object.extend({ className: "DateSet" });
function parseDate(iso8601) { function parseDate(iso8601) {
@@ -289,7 +289,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])("containsAll object array queries", function(done) { it("containsAll object array queries", function(done) {
var MessageSet = Parse.Object.extend({ className: "MessageSet" }); var MessageSet = Parse.Object.extend({ className: "MessageSet" });
@@ -872,7 +872,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])("order by descending number and string", function(done) { it("order by descending number and string", function(done) {
var strings = ["a", "b", "c", "d"]; var strings = ["a", "b", "c", "d"];
var makeBoxedNumber = function(num, i) { var makeBoxedNumber = function(num, i) {
return new BoxedNumber({ number: num, string: strings[i] }); return new BoxedNumber({ number: num, string: strings[i] });
@@ -1579,7 +1579,7 @@ describe('Parse.Query testing', () => {
}) })
}); });
it_exclude_dbs(['postgres'])('properly includes array of mixed objects', (done) => { it('properly includes array of mixed objects', (done) => {
let objects = []; let objects = [];
let total = 0; let total = 0;
while(objects.length != 5) { while(objects.length != 5) {
@@ -2270,7 +2270,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('notEqual with array of pointers', (done) => { it('notEqual with array of pointers', (done) => {
var children = []; var children = [];
var parents = []; var parents = [];
var promises = []; var promises = [];
@@ -2364,7 +2364,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('query match on array with single object', (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]);
@@ -2380,7 +2380,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('query match on array with multiple objects', (done) => { it('query match on array with multiple objects', (done) => {
var target1 = {__type: 'Pointer', className: 'TestObject', objectId: 'abc'}; var target1 = {__type: 'Pointer', className: 'TestObject', objectId: 'abc'};
var target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'}; var target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'};
var obj= new Parse.Object('TestObject'); var obj= new Parse.Object('TestObject');
@@ -2449,7 +2449,7 @@ describe('Parse.Query testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('should find objects with array of pointers', (done) => { it('should find objects with array of pointers', (done) => {
var objects = []; var objects = [];
while(objects.length != 5) { while(objects.length != 5) {
var object = new Parse.Object('ContainedObject'); var object = new Parse.Object('ContainedObject');
@@ -2488,7 +2488,7 @@ describe('Parse.Query testing', () => {
}) })
}) })
it_exclude_dbs(['postgres'])('query with two OR subqueries (regression test #1259)', done => { it('query with two OR subqueries (regression test #1259)', done => {
let relatedObject = new Parse.Object('Class2'); let relatedObject = new Parse.Object('Class2');
relatedObject.save().then(relatedObject => { relatedObject.save().then(relatedObject => {
let anObject = new Parse.Object('Class1'); let anObject = new Parse.Object('Class1');

View File

@@ -296,7 +296,7 @@ describe('Parse.Relation testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])("query on pointer and relation fields with equal", (done) => { it("query on pointer and relation fields with equal", (done) => {
var ChildObject = Parse.Object.extend("ChildObject"); var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = []; var childObjects = [];
for (var i = 0; i < 10; i++) { for (var i = 0; i < 10; i++) {
@@ -377,7 +377,7 @@ describe('Parse.Relation testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])("or queries on pointer and relation fields", (done) => { it("or queries on pointer and relation fields", (done) => {
var ChildObject = Parse.Object.extend("ChildObject"); var ChildObject = Parse.Object.extend("ChildObject");
var childObjects = []; var childObjects = [];
for (var i = 0; i < 10; i++) { for (var i = 0; i < 10; i++) {

View File

@@ -1273,7 +1273,7 @@ describe('Parse.User testing', () => {
// What this means is, only one Parse User can be linked to a // What this means is, only one Parse User can be linked to a
// particular Facebook account. // particular Facebook account.
it_exclude_dbs(['postgres'])("link with provider for already linked user", (done) => { it("link with provider for already linked user", (done) => {
var provider = getMockFacebookProvider(); var provider = getMockFacebookProvider();
Parse.User._registerAuthenticationProvider(provider); Parse.User._registerAuthenticationProvider(provider);
var user = new Parse.User(); var user = new Parse.User();
@@ -1295,7 +1295,10 @@ describe('Parse.User testing', () => {
user2.signUp(null, { user2.signUp(null, {
success: function(model) { success: function(model) {
user2._linkWith('facebook', { user2._linkWith('facebook', {
success: fail, success: (err) => {
jfail(err);
done();
},
error: function(model, error) { error: function(model, error) {
expect(error.code).toEqual( expect(error.code).toEqual(
Parse.Error.ACCOUNT_ALREADY_LINKED); Parse.Error.ACCOUNT_ALREADY_LINKED);
@@ -2066,7 +2069,7 @@ describe('Parse.User testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('get session only for current user', (done) => { it('get session only for current user', (done) => {
Parse.Promise.as().then(() => { Parse.Promise.as().then(() => {
return Parse.User.signUp("test1", "test", { foo: "bar" }); return Parse.User.signUp("test1", "test", { foo: "bar" });
}).then(() => { }).then(() => {
@@ -2094,7 +2097,7 @@ describe('Parse.User testing', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('delete session by object', (done) => { it('delete session by object', (done) => {
Parse.Promise.as().then(() => { Parse.Promise.as().then(() => {
return Parse.User.signUp("test1", "test", { foo: "bar" }); return Parse.User.signUp("test1", "test", { foo: "bar" });
}).then(() => { }).then(() => {

View File

@@ -9,7 +9,7 @@ describe('Pointer Permissions', () => {
new Config(Parse.applicationId).database.schemaCache.clear(); new Config(Parse.applicationId).database.schemaCache.clear();
}); });
it_exclude_dbs(['postgres'])('should work with find', (done) => { it('should work with find', (done) => {
let config = new Config(Parse.applicationId); let config = new Config(Parse.applicationId);
let user = new Parse.User(); let user = new Parse.User();
let user2 = new Parse.User(); let user2 = new Parse.User();
@@ -48,7 +48,7 @@ describe('Pointer Permissions', () => {
}); });
it_exclude_dbs(['postgres'])('should work with write', (done) => { it('should work with write', (done) => {
let config = new Config(Parse.applicationId); let config = new Config(Parse.applicationId);
let user = new Parse.User(); let user = new Parse.User();
let user2 = new Parse.User(); let user2 = new Parse.User();
@@ -113,7 +113,7 @@ describe('Pointer Permissions', () => {
}) })
}); });
it_exclude_dbs(['postgres'])('should let a proper user find', (done) => { it('should let a proper user find', (done) => {
let config = new Config(Parse.applicationId); let config = new Config(Parse.applicationId);
let user = new Parse.User(); let user = new Parse.User();
let user2 = new Parse.User(); let user2 = new Parse.User();
@@ -199,7 +199,7 @@ describe('Pointer Permissions', () => {
}) })
}); });
it_exclude_dbs(['postgres'])('should handle multiple writeUserFields', done => { it('should handle multiple writeUserFields', done => {
let config = new Config(Parse.applicationId); let config = new Config(Parse.applicationId);
let user = new Parse.User(); let user = new Parse.User();
let user2 = new Parse.User(); let user2 = new Parse.User();
@@ -281,7 +281,7 @@ describe('Pointer Permissions', () => {
}) })
}); });
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => { it('tests CLP / Pointer Perms / ACL write (PP Locked)', (done) => {
/* /*
tests: tests:
CLP: update closed ({}) CLP: update closed ({})
@@ -328,7 +328,7 @@ describe('Pointer Permissions', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => { it('tests CLP / Pointer Perms / ACL write (ACL Locked)', (done) => {
/* /*
tests: tests:
CLP: update closed ({}) CLP: update closed ({})
@@ -373,7 +373,7 @@ describe('Pointer Permissions', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => { it('tests CLP / Pointer Perms / ACL write (ACL/PP OK)', (done) => {
/* /*
tests: tests:
CLP: update closed ({}) CLP: update closed ({})
@@ -418,7 +418,7 @@ describe('Pointer Permissions', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => { it('tests CLP / Pointer Perms / ACL read (PP locked)', (done) => {
/* /*
tests: tests:
CLP: find/get open ({}) CLP: find/get open ({})

View File

@@ -357,7 +357,7 @@ describe('PushController', () => {
}) })
}); });
it_exclude_dbs(['postgres'])('should support full RESTQuery for increment', (done) => { it('should support full RESTQuery for increment', (done) => {
var payload = {data: { var payload = {data: {
alert: "Hello World!", alert: "Hello World!",
badge: 'Increment', badge: 'Increment',
@@ -392,7 +392,7 @@ describe('PushController', () => {
pushController.sendPush(payload, where, config, auth).then((result) => { pushController.sendPush(payload, where, config, auth).then((result) => {
done(); done();
}).catch((err) => { }).catch((err) => {
fail('should not fail'); jfail(err);
done(); done();
}); });
}); });

View File

@@ -11,6 +11,11 @@ var config = new Config('test');
let database = config.database; let database = config.database;
describe('rest create', () => { describe('rest create', () => {
beforeEach(() => {
config = new Config('test');
});
it('handles _id', done => { it('handles _id', done => {
rest.create(config, auth.nobody(config), 'Foo', {}) rest.create(config, auth.nobody(config), 'Foo', {})
.then(() => database.adapter.find('Foo', { fields: {} }, {}, {})) .then(() => database.adapter.find('Foo', { fields: {} }, {}, {}))
@@ -167,7 +172,7 @@ describe('rest create', () => {
}); });
}); });
it_exclude_dbs(['postgres'])('handles anonymous user signup and upgrade to new user', (done) => { it('handles anonymous user signup and upgrade to new user', (done) => {
var data1 = { var data1 = {
authData: { authData: {
anonymous: { anonymous: {
@@ -201,7 +206,7 @@ describe('rest create', () => {
expect(r.get('username')).toEqual('hello'); expect(r.get('username')).toEqual('hello');
done(); done();
}).catch((err) => { }).catch((err) => {
fail('should not fail') jfail(err);
done(); done();
}) })
}); });
@@ -227,7 +232,7 @@ describe('rest create', () => {
}) })
}); });
it_exclude_dbs(['postgres'])('test facebook signup and login', (done) => { it('test facebook signup and login', (done) => {
var data = { var data = {
authData: { authData: {
facebook: { facebook: {
@@ -257,16 +262,19 @@ describe('rest create', () => {
var output = response.results[0]; var output = response.results[0];
expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId); expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId);
done(); done();
}); }).catch(err => {
jfail(err);
done();
})
}); });
it_exclude_dbs(['postgres'])('stores pointers', done => { it('stores pointers', done => {
let obj = { let obj = {
foo: 'bar', foo: 'bar',
aPointer: { aPointer: {
__type: 'Pointer', __type: 'Pointer',
className: 'JustThePointer', className: 'JustThePointer',
objectId: 'qwerty' objectId: 'qwerty1234' // make it 10 chars to match PG storage
} }
}; };
rest.create(config, auth.nobody(config), 'APointerDarkly', obj) rest.create(config, auth.nobody(config), 'APointerDarkly', obj)
@@ -283,7 +291,7 @@ describe('rest create', () => {
expect(output.aPointer).toEqual({ expect(output.aPointer).toEqual({
__type: 'Pointer', __type: 'Pointer',
className: 'JustThePointer', className: 'JustThePointer',
objectId: 'qwerty' objectId: 'qwerty1234'
}); });
done(); done();
}); });
@@ -344,7 +352,7 @@ describe('rest create', () => {
}); });
}); });
it_exclude_dbs(['postgres'])("test specified session length", (done) => { it("test specified session length", (done) => {
var user = { var user = {
username: 'asdf', username: 'asdf',
password: 'zxcv', password: 'zxcv',
@@ -376,11 +384,14 @@ describe('rest create', () => {
expect(actual.getHours()).toEqual(expected.getHours()); expect(actual.getHours()).toEqual(expected.getHours());
expect(actual.getMinutes()).toEqual(expected.getMinutes()); expect(actual.getMinutes()).toEqual(expected.getMinutes());
done();
}).catch(err => {
jfail(err);
done(); done();
}); });
}); });
it_exclude_dbs(['postgres'])("can create a session with no expiration", (done) => { it("can create a session with no expiration", (done) => {
var user = { var user = {
username: 'asdf', username: 'asdf',
password: 'zxcv', password: 'zxcv',
@@ -404,6 +415,10 @@ describe('rest create', () => {
expect(session.expiresAt).toBeUndefined(); expect(session.expiresAt).toBeUndefined();
done(); done();
}); }).catch(err => {
console.error(err);
fail(err);
done();
})
}); });
}); });

View File

@@ -10,7 +10,7 @@ global.on_db = (db, callback, elseCallback) => {
return callback(); return callback();
} }
if (elseCallback) { if (elseCallback) {
elseCallback(); return elseCallback();
} }
} }

View File

@@ -1286,10 +1286,8 @@ describe('schemas', () => {
}).then((results) => { }).then((results) => {
expect(results.length).toBe(1); expect(results.length).toBe(1);
done(); done();
}, () => {
fail("should not fail!");
done();
}).catch( (err) => { }).catch( (err) => {
jfail(err);
done(); done();
}) })
}); });
@@ -1351,15 +1349,13 @@ describe('schemas', () => {
}).then((results) => { }).then((results) => {
expect(results.length).toBe(1); expect(results.length).toBe(1);
done(); done();
}, (err) => {
fail("should not fail!");
done();
}).catch( (err) => { }).catch( (err) => {
jfail(err);
done(); done();
}) })
}); });
it_exclude_dbs(['postgres'])('validate CLP 3', done => { it('validate CLP 3', done => {
let user = new Parse.User(); let user = new Parse.User();
user.setUsername('user'); user.setUsername('user');
user.setPassword('user'); user.setPassword('user');
@@ -1411,8 +1407,8 @@ describe('schemas', () => {
}).then((results) => { }).then((results) => {
expect(results.length).toBe(1); expect(results.length).toBe(1);
done(); done();
}, (err) => { }).catch((err) => {
fail("should not fail!"); jfail(err);
done(); done();
}); });
}); });
@@ -1477,10 +1473,8 @@ describe('schemas', () => {
}).then((results) => { }).then((results) => {
expect(results.length).toBe(1); expect(results.length).toBe(1);
done(); done();
}, (err) => {
fail("should not fail!");
done();
}).catch( (err) => { }).catch( (err) => {
jfail(err);
done(); done();
}) })
}); });

View File

@@ -26,6 +26,12 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc
switch(key) { switch(key) {
case 'objectId': case 'objectId':
case '_id': case '_id':
if (className === '_GlobalConfig') {
return {
key: key,
value: parseInt(restValue)
}
}
key = '_id'; key = '_id';
break; break;
case 'createdAt': case 'createdAt':
@@ -143,7 +149,12 @@ function transformQueryKeyValue(className, key, value, schema) {
return {key: '_email_verify_token_expires_at', value: valueAsDate(value)} return {key: '_email_verify_token_expires_at', value: valueAsDate(value)}
} }
break; break;
case 'objectId': return {key: '_id', value} case 'objectId': {
if (className === '_GlobalConfig') {
value = parseInt(value);
}
return {key: '_id', value}
}
case 'sessionToken': return {key: '_session_token', value} case 'sessionToken': return {key: '_session_token', value}
case '_rperm': case '_rperm':
case '_wperm': case '_wperm':

View File

@@ -110,6 +110,31 @@ const toPostgresSchema = (schema) => {
return schema; return schema;
} }
const handleDotFields = (object) => {
Object.keys(object).forEach(fieldName => {
if (fieldName.indexOf('.') > -1) {
let components = fieldName.split('.');
let first = components.shift();
object[first] = object[first] || {};
let currentObj = object[first];
let next;
let value = object[fieldName];
if (value && value.__op === 'Delete') {
value = undefined;
}
while(next = components.shift()) {
currentObj[next] = currentObj[next] || {};
if (components.length === 0) {
currentObj[next] = value;
}
currentObj = currentObj[next];
}
delete object[fieldName];
}
});
return object;
}
// Returns the list of join tables on a schema // Returns the list of join tables on a schema
const joinTablesForSchema = (schema) => { const joinTablesForSchema = (schema) => {
let list = []; let list = [];
@@ -130,8 +155,20 @@ const buildWhereClause = ({ schema, query, index }) => {
schema = toPostgresSchema(schema); schema = toPostgresSchema(schema);
for (let fieldName in query) { for (let fieldName in query) {
let isArrayField = schema.fields
&& schema.fields[fieldName]
&& schema.fields[fieldName].type === 'Array';
let initialPatternsLength = patterns.length; let initialPatternsLength = patterns.length;
let fieldValue = query[fieldName]; let fieldValue = query[fieldName];
// nothingin the schema, it's gonna blow up
if (!schema.fields[fieldName]) {
// as it won't exist
if (fieldValue.$exists === false) {
continue;
}
}
if (fieldName.indexOf('.') >= 0) { if (fieldName.indexOf('.') >= 0) {
let components = fieldName.split('.').map((cmpt, index) => { let components = fieldName.split('.').map((cmpt, index) => {
if (index == 0) { if (index == 0) {
@@ -154,26 +191,34 @@ const buildWhereClause = ({ schema, query, index }) => {
patterns.push(`$${index}:name = $${index + 1}`); patterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue); values.push(fieldName, fieldValue);
index += 2; index += 2;
} else if (fieldName === '$or') { } else if (fieldName === '$or' || fieldName === '$and') {
let clauses = []; let clauses = [];
let clauseValues = []; let clauseValues = [];
fieldValue.forEach((subQuery, idx) =>  { fieldValue.forEach((subQuery, idx) =>  {
let clause = buildWhereClause({ schema, query: subQuery, index }); let clause = buildWhereClause({ schema, query: subQuery, index });
if (clause.pattern.length > 0) {
clauses.push(clause.pattern); clauses.push(clause.pattern);
clauseValues.push(...clause.values); clauseValues.push(...clause.values);
index += clause.values.length; index += clause.values.length;
}
}); });
patterns.push(`(${clauses.join(' OR ')})`); let orOrAnd = fieldName === '$or' ? ' OR ' : ' AND ';
patterns.push(`(${clauses.join(orOrAnd)})`);
values.push(...clauseValues); values.push(...clauseValues);
} }
if (fieldValue.$ne) { if (fieldValue.$ne) {
if (isArrayField) {
fieldValue.$ne = JSON.stringify([fieldValue.$ne]);
patterns.push(`NOT array_contains($${index}:name, $${index + 1})`);
} else {
if (fieldValue.$ne === null) { if (fieldValue.$ne === null) {
patterns.push(`$${index}:name <> $${index + 1}`); patterns.push(`$${index}:name <> $${index + 1}`);
} else { } else {
// if not null, we need to manually exclude null // if not null, we need to manually exclude null
patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`); patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`);
} }
}
// TODO: support arrays // TODO: support arrays
values.push(fieldName, fieldValue.$ne); values.push(fieldName, fieldValue.$ne);
@@ -186,7 +231,10 @@ const buildWhereClause = ({ schema, query, index }) => {
index += 2; index += 2;
} }
const isInOrNin = Array.isArray(fieldValue.$in) || Array.isArray(fieldValue.$nin); const isInOrNin = Array.isArray(fieldValue.$in) || Array.isArray(fieldValue.$nin);
if (Array.isArray(fieldValue.$in) && schema.fields[fieldName].type === 'Array') { if (Array.isArray(fieldValue.$in) &&
isArrayField &&
schema.fields[fieldName].contents &&
schema.fields[fieldName].contents.type === 'String') {
let inPatterns = []; let inPatterns = [];
let allowNull = false; let allowNull = false;
values.push(fieldName); values.push(fieldName);
@@ -207,15 +255,21 @@ const buildWhereClause = ({ schema, query, index }) => {
} else if (isInOrNin) { } else if (isInOrNin) {
var createConstraint = (baseArray, notIn) => { var createConstraint = (baseArray, notIn) => {
if (baseArray.length > 0) { if (baseArray.length > 0) {
let not = notIn ? ' NOT ' : '';
if (isArrayField) {
patterns.push(`${not} array_contains($${index}:name, $${index+1})`);
values.push(fieldName, JSON.stringify(baseArray));
index += 2;
} else {
let inPatterns = []; let inPatterns = [];
values.push(fieldName); values.push(fieldName);
baseArray.forEach((listElem, listIndex) => { baseArray.forEach((listElem, listIndex) => {
values.push(listElem); values.push(listElem);
inPatterns.push(`$${index + 1 + listIndex}`); inPatterns.push(`$${index + 1 + listIndex}`);
}); });
let not = notIn ? 'NOT' : '';
patterns.push(`$${index}:name ${not} IN (${inPatterns.join(',')})`); patterns.push(`$${index}:name ${not} IN (${inPatterns.join(',')})`);
index = index + 1 + inPatterns.length; index = index + 1 + inPatterns.length;
}
} else if (!notIn) { } else if (!notIn) {
values.push(fieldName); values.push(fieldName);
patterns.push(`$${index}:name IS NULL`); patterns.push(`$${index}:name IS NULL`);
@@ -230,24 +284,10 @@ const buildWhereClause = ({ schema, query, index }) => {
} }
} }
if (Array.isArray(fieldValue.$all) && schema.fields[fieldName].type === 'Array') { if (Array.isArray(fieldValue.$all) && isArrayField) {
let inPatterns = []; patterns.push(`array_contains_all($${index}:name, $${index+1}::jsonb)`);
let allowNull = false; values.push(fieldName, JSON.stringify(fieldValue.$all));
values.push(fieldName); index+=2;
fieldValue.$all.forEach((listElem, listIndex) => {
if (listElem === null ) {
allowNull = true;
} else {
values.push(listElem);
inPatterns.push(`$${index + 1 + listIndex - (allowNull ? 1 : 0)}`);
}
});
if (allowNull) {
patterns.push(`($${index}:name IS NULL OR $${index}:name @> array_to_json(ARRAY[${inPatterns.join(',')}]))::jsonb`);
} else {
patterns.push(`$${index}:name @> json_build_array(${inPatterns.join(',')})::jsonb`);
}
index = index + 1 + inPatterns.length;
} }
if (typeof fieldValue.$exists !== 'undefined') { if (typeof fieldValue.$exists !== 'undefined') {
@@ -266,10 +306,22 @@ const buildWhereClause = ({ schema, query, index }) => {
let distanceInKM = distance*6371*1000; let distanceInKM = distance*6371*1000;
patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) <= $${index+3}`); patterns.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) <= $${index+3}`);
sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) ASC`) sorts.push(`ST_distance_sphere($${index}:name::geometry, POINT($${index+1}, $${index+2})::geometry) ASC`)
values.push(fieldName, point.latitude, point.longitude, distanceInKM); values.push(fieldName, point.longitude, point.latitude, distanceInKM);
index += 4; index += 4;
} }
if (fieldValue.$within && fieldValue.$within.$box) {
let box = fieldValue.$within.$box;
let left = box[0].longitude;
let bottom = box[0].latitude;
let right = box[1].longitude;
let top = box[1].latitude;
patterns.push(`$${index}:name::point <@ $${index+1}::box`);
values.push(fieldName, `((${left}, ${bottom}), (${right}, ${top}))`);
index += 2;
}
if (fieldValue.$regex) { if (fieldValue.$regex) {
let regex = fieldValue.$regex; let regex = fieldValue.$regex;
let operator = '~'; let operator = '~';
@@ -285,10 +337,16 @@ const buildWhereClause = ({ schema, query, index }) => {
} }
if (fieldValue.__type === 'Pointer') { if (fieldValue.__type === 'Pointer') {
if (isArrayField) {
patterns.push(`array_contains($${index}:name, $${index + 1})`);
values.push(fieldName, JSON.stringify([fieldValue]));
index += 2;
} else {
patterns.push(`$${index}:name = $${index + 1}`); patterns.push(`$${index}:name = $${index + 1}`);
values.push(fieldName, fieldValue.objectId); values.push(fieldName, fieldValue.objectId);
index += 2; index += 2;
} }
}
if (fieldValue.__type === 'Date') { if (fieldValue.__type === 'Date') {
patterns.push(`$${index}:name = $${index + 1}`); patterns.push(`$${index}:name = $${index + 1}`);
@@ -345,7 +403,7 @@ export class PostgresStorageAdapter {
setClassLevelPermissions(className, CLPs) { setClassLevelPermissions(className, CLPs) {
return this._ensureSchemaCollectionExists().then(() => { return this._ensureSchemaCollectionExists().then(() => {
const values = [className, 'schema', 'classLevelPermissions', CLPs] const values = [className, 'schema', 'classLevelPermissions', JSON.stringify(CLPs)]
return this._client.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values); return this._client.none(`UPDATE "_SCHEMA" SET $2:name = json_object_set_key($2:name, $3::text, $4::jsonb) WHERE "className"=$1 `, values);
}); });
} }
@@ -568,6 +626,9 @@ export class PostgresStorageAdapter {
let valuesArray = []; let valuesArray = [];
schema = toPostgresSchema(schema); schema = toPostgresSchema(schema);
let geoPoints = {}; let geoPoints = {};
object = handleDotFields(object);
Object.keys(object).forEach(fieldName => { Object.keys(object).forEach(fieldName => {
var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/); var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/);
if (authDataMatch) { if (authDataMatch) {
@@ -584,7 +645,11 @@ export class PostgresStorageAdapter {
valuesArray.push(object[fieldName]); valuesArray.push(object[fieldName]);
} }
if (fieldName == '_email_verify_token_expires_at') { if (fieldName == '_email_verify_token_expires_at') {
if (object[fieldName]) {
valuesArray.push(object[fieldName].iso); valuesArray.push(object[fieldName].iso);
} else {
valuesArray.push(null);
}
} }
if (fieldName == '_perishable_token') { if (fieldName == '_perishable_token') {
valuesArray.push(object[fieldName].iso); valuesArray.push(object[fieldName].iso);
@@ -593,7 +658,11 @@ export class PostgresStorageAdapter {
} }
switch (schema.fields[fieldName].type) { switch (schema.fields[fieldName].type) {
case 'Date': case 'Date':
if (object[fieldName]) {
valuesArray.push(object[fieldName].iso); valuesArray.push(object[fieldName].iso);
} else {
valuesArray.push(null);
}
break; break;
case 'Pointer': case 'Pointer':
valuesArray.push(object[fieldName].objectId); valuesArray.push(object[fieldName].objectId);
@@ -638,7 +707,7 @@ export class PostgresStorageAdapter {
}); });
let geoPointsInjects = Object.keys(geoPoints).map((key, idx) => { let geoPointsInjects = Object.keys(geoPoints).map((key, idx) => {
let value = geoPoints[key]; let value = geoPoints[key];
valuesArray.push(value.latitude, value.longitude); valuesArray.push(value.longitude, value.latitude);
let l = valuesArray.length + columnsArray.length; let l = valuesArray.length + columnsArray.length;
return `POINT($${l}, $${l+1})`; return `POINT($${l}, $${l+1})`;
}); });
@@ -683,21 +752,22 @@ export class PostgresStorageAdapter {
} }
}); });
} }
// Return value not currently well specified.
findOneAndUpdate(className, schema, query, update) {
debug('findOneAndUpdate', className, query, update);
return this.updateObjectsByQuery(className, schema, query, update).then((val) => val[0]);
}
// Apply the update to all objects that match the given Parse Query. // Apply the update to all objects that match the given Parse Query.
updateObjectsByQuery(className, schema, query, update) { updateObjectsByQuery(className, schema, query, update) {
debug('updateObjectsByQuery', className, query, update); debug('updateObjectsByQuery', className, query, update);
return this.findOneAndUpdate(className, schema, query, update);
}
// Return value not currently well specified.
findOneAndUpdate(className, schema, query, update) {
debug('findOneAndUpdate', className, query, update);
let conditionPatterns = []; let conditionPatterns = [];
let updatePatterns = []; let updatePatterns = [];
let values = [className] let values = [className]
let index = 2; let index = 2;
schema = toPostgresSchema(schema); schema = toPostgresSchema(schema);
update = handleDotFields(update);
// Resolve authData first, // Resolve authData first,
// So we don't end up with multiple key updates // So we don't end up with multiple key updates
for (let fieldName in update) { for (let fieldName in update) {
@@ -717,7 +787,7 @@ export class PostgresStorageAdapter {
// This recursively sets the json_object // This recursively sets the json_object
// Only 1 level deep // Only 1 level deep
let generate = (jsonb, key, value) => { let generate = (jsonb, key, value) => {
return `json_object_set_key(${jsonb}, ${key}, ${value})::jsonb`;  return `json_object_set_key(COALESCE(${jsonb}, '{}'::jsonb), ${key}, ${value})::jsonb`; 
} }
let lastKey = `$${index}:name`; let lastKey = `$${index}:name`;
let fieldNameIndex = index; let fieldNameIndex = index;
@@ -726,7 +796,15 @@ export class PostgresStorageAdapter {
let update = Object.keys(fieldValue).reduce((lastKey, key) => { let update = Object.keys(fieldValue).reduce((lastKey, key) => {
let str = generate(lastKey, `$${index}::text`, `$${index+1}::jsonb`) let str = generate(lastKey, `$${index}::text`, `$${index+1}::jsonb`)
index+=2; index+=2;
values.push(key, fieldValue[key]); let value = fieldValue[key];
if (value) {
if (value.__op === 'Delete') {
value = null;
} else {
value = JSON.stringify(value)
}
}
values.push(key, value);
return str; return str;
}, lastKey); }, lastKey);
updatePatterns.push(`$${fieldNameIndex}:name = ${update}`); updatePatterns.push(`$${fieldNameIndex}:name = ${update}`);
@@ -810,17 +888,17 @@ export class PostgresStorageAdapter {
let qs = `UPDATE $1:name SET ${updatePatterns.join(',')} WHERE ${where.pattern} RETURNING *`; let qs = `UPDATE $1:name SET ${updatePatterns.join(',')} WHERE ${where.pattern} RETURNING *`;
debug('update: ', qs, values); debug('update: ', qs, values);
return this._client.any(qs, values) return this._client.any(qs, values); // TODO: This is unsafe, verification is needed, or a different query method;
.then(val => val[0]); // TODO: This is unsafe, verification is needed, or a different query method;
} }
// Hopefully, we can get rid of this. It's only used for config and hooks. // Hopefully, we can get rid of this. It's only used for config and hooks.
upsertOneObject(className, schema, query, update) { upsertOneObject(className, schema, query, update) {
debug('upsertOneObject', {className, query, update}); debug('upsertOneObject', {className, query, update});
return this.createObject(className, schema, update).catch((err) => { let createValue = Object.assign({}, query, update);
return this.createObject(className, schema, createValue).catch((err) => {
// ignore duplicate value errors as it's upsert // ignore duplicate value errors as it's upsert
if (err.code == Parse.Error.DUPLICATE_VALUE) { if (err.code == Parse.Error.DUPLICATE_VALUE) {
return; return this.findOneAndUpdate(className, schema, query, update);
} }
throw err; throw err;
}); });
@@ -882,8 +960,8 @@ export class PostgresStorageAdapter {
} }
if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') { if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') {
object[fieldName] = { object[fieldName] = {
latitude: object[fieldName].x, latitude: object[fieldName].y,
longitude: object[fieldName].y longitude: object[fieldName].x
} }
} }
if (object[fieldName] && schema.fields[fieldName].type === 'File') { if (object[fieldName] && schema.fields[fieldName].type === 'File') {
@@ -972,8 +1050,7 @@ export class PostgresStorageAdapter {
throw err; throw err;
}); });
}); });
return Promise.all(promises).then(() => { promises = promises.concat([
return Promise.all([
this._client.any(json_object_set_key).catch((err) => { this._client.any(json_object_set_key).catch((err) => {
console.error(err); console.error(err);
}), }),
@@ -985,9 +1062,15 @@ export class PostgresStorageAdapter {
}), }),
this._client.any(array_remove).catch((err) => { this._client.any(array_remove).catch((err) => {
console.error(err); console.error(err);
}),
this._client.any(array_contains_all).catch((err) => {
console.error(err);
}),
this._client.any(array_contains).catch((err) => {
console.error(err);
}) })
]); ]);
}).then(() => { return Promise.all(promises).then(() => {
debug(`initialzationDone in ${new Date().getTime() - now}`); debug(`initialzationDone in ${new Date().getTime() - now}`);
}) })
} }
@@ -1052,5 +1135,29 @@ AS $function$
SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb; SELECT array_to_json(ARRAY(SELECT * FROM jsonb_array_elements("array") as elt WHERE elt NOT IN (SELECT * FROM (SELECT jsonb_array_elements("values")) AS sub)))::jsonb;
$function$;`; $function$;`;
const array_contains_all = `CREATE OR REPLACE FUNCTION "array_contains_all"(
"array" jsonb,
"values" jsonb
)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT RES.CNT = jsonb_array_length("values") FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES ;
$function$;`;
const array_contains = `CREATE OR REPLACE FUNCTION "array_contains"(
"array" jsonb,
"values" jsonb
)
RETURNS boolean
LANGUAGE sql
IMMUTABLE
STRICT
AS $function$
SELECT RES.CNT >= 1 FROM (SELECT COUNT(*) as CNT FROM jsonb_array_elements("array") as elt WHERE elt IN (SELECT jsonb_array_elements("values"))) as RES ;
$function$;`;
export default PostgresStorageAdapter; export default PostgresStorageAdapter;
module.exports = PostgresStorageAdapter; // Required for tests module.exports = PostgresStorageAdapter; // Required for tests

View File

@@ -92,6 +92,10 @@ const defaultColumns = Object.freeze({
"className": {type:'String'}, "className": {type:'String'},
"triggerName": {type:'String'}, "triggerName": {type:'String'},
"url": {type:'String'} "url": {type:'String'}
},
_GlobalConfig: {
"objectId": {type: 'String'},
"params": {type: 'Object'}
} }
}); });
@@ -265,12 +269,13 @@ const injectDefaultSchema = ({className, fields, classLevelPermissions}) => ({
}); });
const _HooksSchema = {className: "_Hooks", fields: defaultColumns._Hooks}; const _HooksSchema = {className: "_Hooks", fields: defaultColumns._Hooks};
const _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig }
const _PushStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ const _PushStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
className: "_PushStatus", className: "_PushStatus",
fields: {}, fields: {},
classLevelPermissions: {} classLevelPermissions: {}
})); }));
const VolatileClassesSchemas = [_HooksSchema, _PushStatusSchema]; const VolatileClassesSchemas = [_HooksSchema, _PushStatusSchema, _GlobalConfigSchema];
const dbTypeMatchesObjectType = (dbType, objectType) => { const dbTypeMatchesObjectType = (dbType, objectType) => {
if (dbType.type !== objectType.type) return false; if (dbType.type !== objectType.type) return false;

View File

@@ -5,7 +5,7 @@ import * as middleware from "../middlewares";
export class GlobalConfigRouter extends PromiseRouter { export class GlobalConfigRouter extends PromiseRouter {
getGlobalConfig(req) { getGlobalConfig(req) {
return req.config.database.find('_GlobalConfig', { objectId: 1 }, { limit: 1 }).then((results) => { return req.config.database.find('_GlobalConfig', { objectId: "1" }, { limit: 1 }).then((results) => {
if (results.length != 1) { if (results.length != 1) {
// If there is no config in the database - return empty config. // If there is no config in the database - return empty config.
return { response: { params: {} } }; return { response: { params: {} } };
@@ -22,7 +22,7 @@ export class GlobalConfigRouter extends PromiseRouter {
acc[`params.${key}`] = params[key]; acc[`params.${key}`] = params[key];
return acc; return acc;
}, {}); }, {});
return req.config.database.update('_GlobalConfig', {objectId: 1}, update, {upsert: true}).then(() => ({ response: { result: true } })); return req.config.database.update('_GlobalConfig', {objectId: "1"}, update, {upsert: true}).then(() => ({ response: { result: true } }));
} }
mountRoutes() { mountRoutes() {