From 634d672ad1198dbf44efef5acb267c50c29c7127 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Sat, 11 Jun 2016 00:43:02 -0700 Subject: [PATCH] passing another test --- spec/ParseAPI.spec.js | 13 +-- .../Storage/Mongo/MongoStorageAdapter.js | 4 +- .../Postgres/PostgresStorageAdapter.js | 109 ++++++++++++++++-- src/Controllers/DatabaseController.js | 33 ++++-- src/RestWrite.js | 6 +- src/rest.js | 3 +- 6 files changed, 134 insertions(+), 34 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 28a7d856..3ebbbb6e 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -300,7 +300,7 @@ describe('miscellaneous', function() { }); }); - fit('succeed in logging in', function(done) { + it('succeed in logging in', function(done) { createTestUser(function(u) { expect(typeof u.id).toEqual('string'); @@ -318,7 +318,7 @@ describe('miscellaneous', function() { }, fail); }); - it('increment with a user object', function(done) { + fit('increment with a user object', function(done) { createTestUser().then((user) => { user.increment('foo'); return user.save(); @@ -328,15 +328,14 @@ describe('miscellaneous', function() { expect(user.get('foo')).toEqual(1); user.increment('foo'); return user.save(); - }).then(() => { - Parse.User.logOut(); - return Parse.User.logIn('test', 'moon-y'); - }).then((user) => { + }).then(() => Parse.User.logOut()) + .then(() => Parse.User.logIn('test', 'moon-y')) + .then((user) => { expect(user.get('foo')).toEqual(2); Parse.User.logOut() .then(done); }, (error) => { - fail(error); + fail(JSON.stringify(error)); done(); }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 55710aaa..24216ec6 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -232,7 +232,7 @@ export class MongoStorageAdapter { } // Atomically finds and updates an object based on query. - // Resolve with the updated object. + // Return value not currently well specified. findOneAndUpdate(className, schema, query, update) { const mongoUpdate = transformUpdate(className, update, schema); const mongoWhere = transformWhere(className, query, schema); @@ -255,7 +255,7 @@ export class MongoStorageAdapter { let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema)); return this._adaptiveCollection(className) .then(collection => collection.find(mongoWhere, { skip, limit, sort: mongoSort })) - .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))); + .then(objects => objects.map(object => mongoObjectToParseObject(className, object, schema))) } // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 3006910a..70c51093 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -10,6 +10,7 @@ const parseTypeToPostgresType = type => { case 'Object': return 'jsonb'; case 'Boolean': return 'boolean'; case 'Pointer': return 'char(10)'; + case 'Number': return 'double precision'; case 'Array': if (type.contents && type.contents.type === 'String') { return 'text[]'; @@ -69,8 +70,8 @@ export class PostgresStorageAdapter { } addFieldIfNotExists(className, fieldName, type) { - // TODO: Doing this in a transaction is probably a good idea. - return this._client.query('ALTER TABLE $ ADD COLUMN $ text', { className, fieldName }) + // TODO: Doing this in a transaction might be a good idea. + return this._client.query('ALTER TABLE $ ADD COLUMN $ $', { className, fieldName, postgresType: parseTypeToPostgresType(type) }) .catch(error => { if (error.code === PostgresRelationDoesNotExistError) { return this.createClass(className, { fields: { [fieldName]: type } }) @@ -161,7 +162,7 @@ export class PostgresStorageAdapter { }); } - // TODO: remove the mongo format dependency + // TODO: remove the mongo format dependency in the return value createObject(className, schema, object) { let columnsArray = []; let valuesArray = []; @@ -181,7 +182,7 @@ export class PostgresStorageAdapter { }); let columnsPattern = columnsArray.map((col, index) => `$${index + 2}:name`).join(','); let valuesPattern = valuesArray.map((val, index) => `$${index + 2 + columnsArray.length}`).join(','); - return this._client.query(`INSERT INTO $1~ (${columnsPattern}) VALUES (${valuesPattern})`, [className, ...columnsArray, ...valuesArray]) + return this._client.query(`INSERT INTO $1:name (${columnsPattern}) VALUES (${valuesPattern})`, [className, ...columnsArray, ...valuesArray]) .then(() => ({ ops: [object] })) } @@ -204,9 +205,53 @@ export class PostgresStorageAdapter { return Promise.reject('Not implented yet.') } - // Hopefully we can get rid of this in favor of updateObjectsByQuery. + // Return value not currently well specified. findOneAndUpdate(className, schema, query, update) { - return Promise.reject('Not implented yet.') + let conditionPatterns = []; + let updatePatterns = []; + let values = [] + values.push(className); + let index = 2; + + for (let fieldName in update) { + let fieldValue = update[fieldName]; + if (fieldValue.__op === 'Increment') { + updatePatterns.push(`$${index}:name = COALESCE($${index}:name, 0) + $${index + 1}`); + values.push(fieldName, fieldValue.amount); + index += 2; + } else if (fieldName === 'updatedAt') { //TODO: stop special casing this. It should check for __type === 'Date' and use .iso + updatePatterns.push(`$${index}:name = $${index + 1}`) + values.push(fieldName, new Date(fieldValue)); + index += 2; + } else { + return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support this type of update yet`)); + } + } + + for (let fieldName in query) { + let fieldValue = query[fieldName]; + if (typeof fieldValue === 'string') { + conditionPatterns.push(`$${index}:name = $${index + 1}`); + values.push(fieldName, fieldValue); + index += 2; + } else if (Array.isArray(fieldValue.$in)) { + let inPatterns = []; + values.push(fieldName); + fieldValue.$in.forEach((listElem, listIndex) => { + values.push(listElem); + inPatterns.push(`$${index + 1 + listIndex}`); + }); + conditionPatterns.push(`$${index}:name && ARRAY[${inPatterns.join(',')}]`); + index = index + 1 + inPatterns.length; + } else { + return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, `Postgres doesn't support this type of request yet`)); + } + } + let qs = `UPDATE $1:name SET ${updatePatterns.join(',')} WHERE ${conditionPatterns.join(' AND ')} RETURNING *`; + return this._client.query(qs, values) + .then(val => { + return val[0]; + }) } // Hopefully we can get rid of this. It's only used for config and hooks. @@ -216,11 +261,61 @@ export class PostgresStorageAdapter { // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. find(className, schema, query, { skip, limit, sort }) { - return this._client.query("SELECT * FROM $", { className }) + let conditionPatterns = []; + let values = []; + values.push(className); + let index = 2; + + for (let fieldName in query) { + let fieldValue = query[fieldName]; + if (typeof fieldValue === 'string') { + conditionPatterns.push(`$${index}:name = $${index + 1}`); + values.push(fieldName, fieldValue); + index += 2; + } else if (fieldValue.$ne) { + conditionPatterns.push(`$${index}:name <> $${index + 1}`); + values.push(fieldName, fieldValue.$ne) + index += 2; + } else if (Array.isArray(fieldValue.$in)) { + let inPatterns = []; + values.push(fieldName); + fieldValue.$in.forEach((listElem, listIndex) => { + values.push(listElem); + inPatterns.push(`$${index + 1 + listIndex}`); + }); + conditionPatterns.push(`$${index}:name IN (${inPatterns.join(',')})`); + index = index + 1 + inPatterns.length; + } else if (fieldValue.__type === 'Pointer') { + conditionPatterns.push(`$${index}:name = $${index + 1}`); + values.push(fieldName, fieldValue.objectId); + index += 2; + } else { + return Promise.reject(new Parse.Error(Parse.Error.OPERATION_FORBIDDEN, "Postgres doesn't support this query type yet")); + } + } + + return this._client.query(`SELECT * FROM $1:name WHERE ${conditionPatterns.join(' AND ')}`, values) .then(results => results.map(object => { Object.keys(schema.fields).filter(field => schema.fields[field].type === 'Pointer').forEach(fieldName => { object[fieldName] = { objectId: object[fieldName], __type: 'Pointer', className: schema.fields[fieldName].targetClass }; }); + //TODO: remove this reliance on the mongo format. DB adapter shouldn't know there is a difference between created at and any other date field. + if (object.createdAt) { + object.createdAt = object.createdAt.toISOString(); + } + if (object.updatedAt) { + object.updatedAt = object.updatedAt.toISOString(); + } + if (object.expiresAt) { + object.expiresAt = { __type: 'Date', iso: object.expiresAt.toISOString() }; + } + + for (let fieldName in object) { + if (object[fieldName] === null) { + delete object[fieldName]; + } + } + return object; })) } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index bed0e154..221f6a7e 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -240,7 +240,7 @@ DatabaseController.prototype.update = function(className, query, update, { } else if (upsert) { return this.adapter.upsertOneObject(className, schema, query, update); } else { - return this.adapter.findOneAndUpdate(className, schema, query, update); + return this.adapter.findOneAndUpdate(className, schema, query, update) } }); }) @@ -645,13 +645,15 @@ DatabaseController.prototype.find = function(className, query, { let isMaster = acl === undefined; let aclGroup = acl || []; let op = typeof query.objectId == 'string' && Object.keys(query).length === 1 ? 'get' : 'find'; + let classExists = true; return this.loadSchema() .then(schemaController => { return schemaController.getOneSchema(className) .catch(error => { - // If the schema doesn't exist, pretend it exists with no fields. This behaviour - // will likely need revisiting. + // Behaviour for non-existent classes is kinda weird on Parse.com. Probably doesn't matter too much. + // For now, pretend the class exists but has no objects, if (error === undefined) { + classExists = false; return { fields: {} }; } throw error; @@ -685,10 +687,9 @@ DatabaseController.prototype.find = function(className, query, { } if (!query) { if (op == 'get') { - return Promise.reject(new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, - 'Object not found.')); + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); } else { - return Promise.resolve([]); + return []; } } if (!isMaster) { @@ -696,13 +697,21 @@ DatabaseController.prototype.find = function(className, query, { } validateQuery(query); if (count) { - return this.adapter.count(className, schema, query); + if (!classExists) { + return 0; + } else { + return this.adapter.count(className, schema, query); + } } else { - return this.adapter.find(className, schema, query, { skip, limit, sort }) - .then(objects => objects.map(object => { - object = untransformObjectACL(object); - return filterSensitiveData(isMaster, aclGroup, className, object) - })); + if (!classExists) { + return []; + } else { + return this.adapter.find(className, schema, query, { skip, limit, sort }) + .then(objects => objects.map(object => { + object = untransformObjectACL(object); + return filterSensitiveData(isMaster, aclGroup, className, object) + })); + } } }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 94799910..ebd7a2d2 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -31,8 +31,7 @@ function RestWrite(config, auth, className, query, data, originalData) { this.runOptions = {}; if (!query && data.objectId) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId ' + - 'is an invalid field name.'); + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'objectId is an invalid field name.'); } // When the operation is complete, this.response may have several @@ -712,8 +711,7 @@ RestWrite.prototype.runDatabaseOperation = function() { if (this.className === '_User' && this.query && !this.auth.couldUpdateUserId(this.query.objectId)) { - throw new Parse.Error(Parse.Error.SESSION_MISSING, - 'cannot modify user ' + this.query.objectId); + throw new Parse.Error(Parse.Error.SESSION_MISSING, `Cannot modify user ${this.query.objectId}.`); } if (this.className === '_Product' && this.data.download) { diff --git a/src/rest.js b/src/rest.js index 45f0d7db..42595295 100644 --- a/src/rest.js +++ b/src/rest.js @@ -117,8 +117,7 @@ function update(config, auth, className, objectId, restObject) { originalRestObject = response.results[0]; } - var write = new RestWrite(config, auth, className, - {objectId: objectId}, restObject, originalRestObject); + var write = new RestWrite(config, auth, className, {objectId: objectId}, restObject, originalRestObject); return write.execute(); }); }