diff --git a/spec/InstallationsRouter.spec.js b/spec/InstallationsRouter.spec.js index 3ebfbca68f..2a114309a7 100644 --- a/spec/InstallationsRouter.spec.js +++ b/spec/InstallationsRouter.spec.js @@ -75,8 +75,7 @@ describe('InstallationsRouter', () => { expect(results.length).toEqual(1); done(); }).catch((err) => { - console.error(err); - fail(JSON.stringify(err)); + jfail(err); done(); }); }); diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 6160c03c41..eebb940916 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -113,7 +113,7 @@ describe('miscellaneous', function() { .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 numCreated = 0; 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'); config.database.adapter.addFieldIfNotExists('_User', 'randomField', { type: 'String' }) .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() }) .catch(error => { - console.error(error); expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); 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=' }); file.save() .then(f => { @@ -1495,8 +1494,10 @@ it_exclude_dbs(['postgres'])('ensure that if you try to sign up a user with a un 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 = { 'X-Parse-Application-Id': 'test', 'X-Parse-REST-API-Key': 'rest' diff --git a/spec/ParseGeoPoint.spec.js b/spec/ParseGeoPoint.spec.js index ffda2b512c..c0f66bfa87 100644 --- a/spec/ParseGeoPoint.spec.js +++ b/spec/ParseGeoPoint.spec.js @@ -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 southwestOfSF = new Parse.GeoPoint(37.708813, -122.526398); var northeastOfSF = new Parse.GeoPoint(37.822802, -122.373962); diff --git a/spec/ParseGlobalConfig.spec.js b/spec/ParseGlobalConfig.spec.js index 04f47ac3a9..77d6316174 100644 --- a/spec/ParseGlobalConfig.spec.js +++ b/spec/ParseGlobalConfig.spec.js @@ -7,15 +7,24 @@ let Config = require('../src/Config'); describe('a GlobalConfig', () => { beforeEach(done => { 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( '_GlobalConfig', - { fields: {} }, - { objectId: 1 }, + { fields: { objectId: { type: 'Number' }, params: {type: 'Object'}} }, + query, { 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({ url : 'http://localhost:8378/1/config', 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({ url : 'http://localhost:8378/1/config', 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({ url : 'http://localhost:8378/1/config', 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({ url : 'http://localhost:8378/1/config', 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'); config.database.adapter.deleteObjectsByQuery( '_GlobalConfig', { fields: { params: { __type: 'String' } } }, - { objectId: 1 } + { objectId: "1" } ).then(() => { request.get({ url : 'http://localhost:8378/1/config', diff --git a/spec/ParseObject.spec.js b/spec/ParseObject.spec.js index 888b3cf77b..305cdde5b7 100644 --- a/spec/ParseObject.spec.js +++ b/spec/ParseObject.spec.js @@ -665,13 +665,7 @@ describe('Parse.Object testing', () => { expect(x3.get('stuff')).toEqual([1, {'foo': 'bar'}]); done(); }, (error) => { - console.error(error); - on_db('mongo', () => { - jfail(error); - }); - on_db('postgres', () => { - expect(error.message).toEqual("Postgres does not support Remove operator."); - }); + jfail(error); done(); }); }); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 2d6cbc857a..a0927fcc43 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -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" }); 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" }); @@ -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 makeBoxedNumber = function(num, 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 total = 0; 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 parents = []; 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 obj = new Parse.Object('TestObject'); 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 target2 = {__type: 'Pointer', className: 'TestObject', objectId: '123'}; 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 = []; while(objects.length != 5) { 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'); relatedObject.save().then(relatedObject => { let anObject = new Parse.Object('Class1'); diff --git a/spec/ParseRelation.spec.js b/spec/ParseRelation.spec.js index 054ac86e13..a127e8d517 100644 --- a/spec/ParseRelation.spec.js +++ b/spec/ParseRelation.spec.js @@ -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 childObjects = []; 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 childObjects = []; for (var i = 0; i < 10; i++) { diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 829d12158d..939a21086f 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -1273,7 +1273,7 @@ describe('Parse.User testing', () => { // What this means is, only one Parse User can be linked to a // 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(); Parse.User._registerAuthenticationProvider(provider); var user = new Parse.User(); @@ -1295,7 +1295,10 @@ describe('Parse.User testing', () => { user2.signUp(null, { success: function(model) { user2._linkWith('facebook', { - success: fail, + success: (err) => { + jfail(err); + done(); + }, error: function(model, error) { expect(error.code).toEqual( 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(() => { return Parse.User.signUp("test1", "test", { foo: "bar" }); }).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(() => { return Parse.User.signUp("test1", "test", { foo: "bar" }); }).then(() => { diff --git a/spec/PointerPermissions.spec.js b/spec/PointerPermissions.spec.js index 27041a4de4..f760c4ea68 100644 --- a/spec/PointerPermissions.spec.js +++ b/spec/PointerPermissions.spec.js @@ -9,7 +9,7 @@ describe('Pointer Permissions', () => { 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 user = 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 user = 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 user = 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 user = 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: 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: 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: 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: CLP: find/get open ({}) diff --git a/spec/PushController.spec.js b/spec/PushController.spec.js index 11d516c1c1..1a45ff35bc 100644 --- a/spec/PushController.spec.js +++ b/spec/PushController.spec.js @@ -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: { alert: "Hello World!", badge: 'Increment', @@ -392,7 +392,7 @@ describe('PushController', () => { pushController.sendPush(payload, where, config, auth).then((result) => { done(); }).catch((err) => { - fail('should not fail'); + jfail(err); done(); }); }); diff --git a/spec/RestCreate.spec.js b/spec/RestCreate.spec.js index fbc953ef65..893952313f 100644 --- a/spec/RestCreate.spec.js +++ b/spec/RestCreate.spec.js @@ -11,6 +11,11 @@ var config = new Config('test'); let database = config.database; describe('rest create', () => { + + beforeEach(() => { + config = new Config('test'); + }); + it('handles _id', done => { rest.create(config, auth.nobody(config), 'Foo', {}) .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 = { authData: { anonymous: { @@ -201,7 +206,7 @@ describe('rest create', () => { expect(r.get('username')).toEqual('hello'); done(); }).catch((err) => { - fail('should not fail') + jfail(err); 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 = { authData: { facebook: { @@ -257,16 +262,19 @@ describe('rest create', () => { var output = response.results[0]; expect(output.user.objectId).toEqual(newUserSignedUpByFacebookObjectId); done(); - }); + }).catch(err => { + jfail(err); + done(); + }) }); - it_exclude_dbs(['postgres'])('stores pointers', done => { + it('stores pointers', done => { let obj = { foo: 'bar', aPointer: { __type: 'Pointer', className: 'JustThePointer', - objectId: 'qwerty' + objectId: 'qwerty1234' // make it 10 chars to match PG storage } }; rest.create(config, auth.nobody(config), 'APointerDarkly', obj) @@ -283,7 +291,7 @@ describe('rest create', () => { expect(output.aPointer).toEqual({ __type: 'Pointer', className: 'JustThePointer', - objectId: 'qwerty' + objectId: 'qwerty1234' }); 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 = { username: 'asdf', password: 'zxcv', @@ -376,11 +384,14 @@ describe('rest create', () => { expect(actual.getHours()).toEqual(expected.getHours()); expect(actual.getMinutes()).toEqual(expected.getMinutes()); + done(); + }).catch(err => { + jfail(err); done(); }); }); - it_exclude_dbs(['postgres'])("can create a session with no expiration", (done) => { + it("can create a session with no expiration", (done) => { var user = { username: 'asdf', password: 'zxcv', @@ -404,6 +415,10 @@ describe('rest create', () => { expect(session.expiresAt).toBeUndefined(); done(); - }); + }).catch(err => { + console.error(err); + fail(err); + done(); + }) }); }); diff --git a/spec/helper.js b/spec/helper.js index a42f324ec2..c281404f06 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -10,7 +10,7 @@ global.on_db = (db, callback, elseCallback) => { return callback(); } if (elseCallback) { - elseCallback(); + return elseCallback(); } } diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 9f53307817..83f7aa45b0 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1286,10 +1286,8 @@ describe('schemas', () => { }).then((results) => { expect(results.length).toBe(1); done(); - }, () => { - fail("should not fail!"); - done(); }).catch( (err) => { + jfail(err); done(); }) }); @@ -1351,15 +1349,13 @@ describe('schemas', () => { }).then((results) => { expect(results.length).toBe(1); done(); - }, (err) => { - fail("should not fail!"); - done(); }).catch( (err) => { + jfail(err); done(); }) }); - it_exclude_dbs(['postgres'])('validate CLP 3', done => { + it('validate CLP 3', done => { let user = new Parse.User(); user.setUsername('user'); user.setPassword('user'); @@ -1411,8 +1407,8 @@ describe('schemas', () => { }).then((results) => { expect(results.length).toBe(1); done(); - }, (err) => { - fail("should not fail!"); + }).catch((err) => { + jfail(err); done(); }); }); @@ -1477,10 +1473,8 @@ describe('schemas', () => { }).then((results) => { expect(results.length).toBe(1); done(); - }, (err) => { - fail("should not fail!"); - done(); }).catch( (err) => { + jfail(err); done(); }) }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index b70905ca12..02595a6904 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -26,6 +26,12 @@ const transformKeyValueForUpdate = (className, restKey, restValue, parseFormatSc switch(key) { case 'objectId': case '_id': + if (className === '_GlobalConfig') { + return { + key: key, + value: parseInt(restValue) + } + } key = '_id'; break; case 'createdAt': @@ -143,7 +149,12 @@ function transformQueryKeyValue(className, key, value, schema) { return {key: '_email_verify_token_expires_at', value: valueAsDate(value)} } 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 '_rperm': case '_wperm': diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index b9acedb2ad..8d99818438 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -110,6 +110,31 @@ const toPostgresSchema = (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 const joinTablesForSchema = (schema) => { let list = []; @@ -130,8 +155,20 @@ const buildWhereClause = ({ schema, query, index }) => { schema = toPostgresSchema(schema); for (let fieldName in query) { + let isArrayField = schema.fields + && schema.fields[fieldName] + && schema.fields[fieldName].type === 'Array'; let initialPatternsLength = patterns.length; 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) { let components = fieldName.split('.').map((cmpt, index) => { if (index == 0) { @@ -154,25 +191,33 @@ const buildWhereClause = ({ schema, query, index }) => { patterns.push(`$${index}:name = $${index + 1}`); values.push(fieldName, fieldValue); index += 2; - } else if (fieldName === '$or') { + } else if (fieldName === '$or' || fieldName === '$and') { let clauses = []; let clauseValues = []; fieldValue.forEach((subQuery, idx) =>  { let clause = buildWhereClause({ schema, query: subQuery, index }); - clauses.push(clause.pattern); - clauseValues.push(...clause.values); - index += clause.values.length; + if (clause.pattern.length > 0) { + clauses.push(clause.pattern); + clauseValues.push(...clause.values); + index += clause.values.length; + } }); - patterns.push(`(${clauses.join(' OR ')})`); + let orOrAnd = fieldName === '$or' ? ' OR ' : ' AND '; + patterns.push(`(${clauses.join(orOrAnd)})`); values.push(...clauseValues); } if (fieldValue.$ne) { - if (fieldValue.$ne === null) { - patterns.push(`$${index}:name <> $${index + 1}`); + if (isArrayField) { + fieldValue.$ne = JSON.stringify([fieldValue.$ne]); + patterns.push(`NOT array_contains($${index}:name, $${index + 1})`); } else { - // if not null, we need to manually exclude null - patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`); + if (fieldValue.$ne === null) { + patterns.push(`$${index}:name <> $${index + 1}`); + } else { + // if not null, we need to manually exclude null + patterns.push(`($${index}:name <> $${index + 1} OR $${index}:name IS NULL)`); + } } // TODO: support arrays @@ -186,7 +231,10 @@ const buildWhereClause = ({ schema, query, index }) => { index += 2; } 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 allowNull = false; values.push(fieldName); @@ -207,15 +255,21 @@ const buildWhereClause = ({ schema, query, index }) => { } else if (isInOrNin) { var createConstraint = (baseArray, notIn) => { if (baseArray.length > 0) { - let inPatterns = []; - values.push(fieldName); - baseArray.forEach((listElem, listIndex) => { - values.push(listElem); - inPatterns.push(`$${index + 1 + listIndex}`); - }); - let not = notIn ? 'NOT' : ''; - patterns.push(`$${index}:name ${not} IN (${inPatterns.join(',')})`); - index = index + 1 + inPatterns.length; + 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 = []; + values.push(fieldName); + baseArray.forEach((listElem, listIndex) => { + values.push(listElem); + inPatterns.push(`$${index + 1 + listIndex}`); + }); + patterns.push(`$${index}:name ${not} IN (${inPatterns.join(',')})`); + index = index + 1 + inPatterns.length; + } } else if (!notIn) { values.push(fieldName); 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') { - let inPatterns = []; - let allowNull = false; - values.push(fieldName); - 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 (Array.isArray(fieldValue.$all) && isArrayField) { + patterns.push(`array_contains_all($${index}:name, $${index+1}::jsonb)`); + values.push(fieldName, JSON.stringify(fieldValue.$all)); + index+=2; } if (typeof fieldValue.$exists !== 'undefined') { @@ -266,10 +306,22 @@ const buildWhereClause = ({ schema, query, index }) => { let distanceInKM = distance*6371*1000; 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`) - values.push(fieldName, point.latitude, point.longitude, distanceInKM); + values.push(fieldName, point.longitude, point.latitude, distanceInKM); 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) { let regex = fieldValue.$regex; let operator = '~'; @@ -285,9 +337,15 @@ const buildWhereClause = ({ schema, query, index }) => { } if (fieldValue.__type === 'Pointer') { - patterns.push(`$${index}:name = $${index + 1}`); - values.push(fieldName, fieldValue.objectId); - index += 2; + 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}`); + values.push(fieldName, fieldValue.objectId); + index += 2; + } } if (fieldValue.__type === 'Date') { @@ -345,7 +403,7 @@ export class PostgresStorageAdapter { setClassLevelPermissions(className, CLPs) { 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); }); } @@ -568,6 +626,9 @@ export class PostgresStorageAdapter { let valuesArray = []; schema = toPostgresSchema(schema); let geoPoints = {}; + + object = handleDotFields(object); + Object.keys(object).forEach(fieldName => { var authDataMatch = fieldName.match(/^_auth_data_([a-zA-Z0-9_]+)$/); if (authDataMatch) { @@ -584,7 +645,11 @@ export class PostgresStorageAdapter { valuesArray.push(object[fieldName]); } if (fieldName == '_email_verify_token_expires_at') { - valuesArray.push(object[fieldName].iso); + if (object[fieldName]) { + valuesArray.push(object[fieldName].iso); + } else { + valuesArray.push(null); + } } if (fieldName == '_perishable_token') { valuesArray.push(object[fieldName].iso); @@ -593,7 +658,11 @@ export class PostgresStorageAdapter { } switch (schema.fields[fieldName].type) { case 'Date': - valuesArray.push(object[fieldName].iso); + if (object[fieldName]) { + valuesArray.push(object[fieldName].iso); + } else { + valuesArray.push(null); + } break; case 'Pointer': valuesArray.push(object[fieldName].objectId); @@ -638,7 +707,7 @@ export class PostgresStorageAdapter { }); let geoPointsInjects = Object.keys(geoPoints).map((key, idx) => { let value = geoPoints[key]; - valuesArray.push(value.latitude, value.longitude); + valuesArray.push(value.longitude, value.latitude); let l = valuesArray.length + columnsArray.length; 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. updateObjectsByQuery(className, schema, 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 updatePatterns = []; let values = [className] let index = 2; schema = toPostgresSchema(schema); + + update = handleDotFields(update); // Resolve authData first, // So we don't end up with multiple key updates for (let fieldName in update) { @@ -717,7 +787,7 @@ export class PostgresStorageAdapter { // This recursively sets the json_object // Only 1 level deep 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 fieldNameIndex = index; @@ -726,7 +796,15 @@ export class PostgresStorageAdapter { let update = Object.keys(fieldValue).reduce((lastKey, key) => { let str = generate(lastKey, `$${index}::text`, `$${index+1}::jsonb`) 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; }, lastKey); updatePatterns.push(`$${fieldNameIndex}:name = ${update}`); @@ -810,17 +888,17 @@ export class PostgresStorageAdapter { let qs = `UPDATE $1:name SET ${updatePatterns.join(',')} WHERE ${where.pattern} RETURNING *`; debug('update: ', qs, values); - return this._client.any(qs, values) - .then(val => val[0]); // TODO: This is unsafe, verification is needed, or a different query method; + return this._client.any(qs, values); // 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. upsertOneObject(className, schema, 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 if (err.code == Parse.Error.DUPLICATE_VALUE) { - return; + return this.findOneAndUpdate(className, schema, query, update); } throw err; }); @@ -882,8 +960,8 @@ export class PostgresStorageAdapter { } if (object[fieldName] && schema.fields[fieldName].type === 'GeoPoint') { object[fieldName] = { - latitude: object[fieldName].x, - longitude: object[fieldName].y + latitude: object[fieldName].y, + longitude: object[fieldName].x } } if (object[fieldName] && schema.fields[fieldName].type === 'File') { @@ -972,8 +1050,7 @@ export class PostgresStorageAdapter { throw err; }); }); - return Promise.all(promises).then(() => { - return Promise.all([ + promises = promises.concat([ this._client.any(json_object_set_key).catch((err) => { console.error(err); }), @@ -985,9 +1062,15 @@ export class PostgresStorageAdapter { }), this._client.any(array_remove).catch((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}`); }) } @@ -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; $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; module.exports = PostgresStorageAdapter; // Required for tests diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index ee98ad3d81..f63ab4c829 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -92,6 +92,10 @@ const defaultColumns = Object.freeze({ "className": {type:'String'}, "triggerName": {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 _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig } const _PushStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({ className: "_PushStatus", fields: {}, classLevelPermissions: {} })); -const VolatileClassesSchemas = [_HooksSchema, _PushStatusSchema]; +const VolatileClassesSchemas = [_HooksSchema, _PushStatusSchema, _GlobalConfigSchema]; const dbTypeMatchesObjectType = (dbType, objectType) => { if (dbType.type !== objectType.type) return false; diff --git a/src/Routers/GlobalConfigRouter.js b/src/Routers/GlobalConfigRouter.js index 30a0e11395..b6b856de04 100644 --- a/src/Routers/GlobalConfigRouter.js +++ b/src/Routers/GlobalConfigRouter.js @@ -5,7 +5,7 @@ import * as middleware from "../middlewares"; export class GlobalConfigRouter extends PromiseRouter { 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 there is no config in the database - return empty config. return { response: { params: {} } }; @@ -22,7 +22,7 @@ export class GlobalConfigRouter extends PromiseRouter { acc[`params.${key}`] = params[key]; 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() {