From f75c8b3a4dddc45186b45688cd2ff43f25680768 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 6 Jun 2016 13:47:11 -0700 Subject: [PATCH 01/13] index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)" reconfigure username/email tests Fix broken cloud code Save callback to variable undo Fix all tests where connections are left open after server closes. Fix issues caused by missing gridstore adapter remove uses of _collection reorder find() arguments Accept a database adapter as a parameter sudo maybe? use postgres username reorder find() arguments Build objects with default fields correctly Don't tell adapter about ACL WIP --- spec/ParseAPI.spec.js | 12 ++++--- spec/ParseInstallation.spec.js | 6 ++-- spec/ParseUser.spec.js | 2 +- spec/helper.js | 2 -- .../Storage/Mongo/MongoSchemaCollection.js | 7 +++- .../Postgres/PostgresStorageAdapter.js | 33 ++++++++++++++++--- src/Controllers/SchemaController.js | 33 ++++++++++++------- 7 files changed, 68 insertions(+), 27 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 7bba6c9b74..64d49053ab 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -37,19 +37,22 @@ describe('miscellaneous', function() { expect(obj2.id).toEqual(obj.id); done(); }, - error: fail + error: error => { + fail(JSON.stringify(error)); + done(); + } }); }); }); - it('create a valid parse user', function(done) { + fit('create a valid parse user', function(done) { createTestUser(function(data) { expect(data.id).not.toBeUndefined(); expect(data.getSessionToken()).not.toBeUndefined(); expect(data.get('password')).toBeUndefined(); done(); - }, function(err) { - fail(err); + }, error => { + fail(JSON.stringify(error)); done(); }); }); @@ -86,7 +89,6 @@ describe('miscellaneous', function() { }); it('ensure that email is uniquely indexed', done => { - let numCreated = 0; let numFailed = 0; let user1 = new Parse.User(); diff --git a/spec/ParseInstallation.spec.js b/spec/ParseInstallation.spec.js index 3d1e45d67b..7663e9a48f 100644 --- a/spec/ParseInstallation.spec.js +++ b/spec/ParseInstallation.spec.js @@ -24,7 +24,7 @@ describe('Installations', () => { 'deviceType': device }; rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => config.database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) .then(results => { expect(results.length).toEqual(1); var obj = results[0]; @@ -42,7 +42,7 @@ describe('Installations', () => { 'deviceType': device }; rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => config.database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) .then(results => { expect(results.length).toEqual(1); var obj = results[0]; @@ -60,7 +60,7 @@ describe('Installations', () => { 'deviceType': device }; rest.create(config, auth.nobody(config), '_Installation', input) - .then(() => config.database.adapter.find('_Installation', installationSchema, {}, {})) + .then(() => database.adapter.find('_Installation', installationSchema, {}, {})) .then(results => { expect(results.length).toEqual(1); var obj = results[0]; diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 1131e95c0c..91a3b9a357 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -2288,7 +2288,7 @@ describe('Parse.User testing', () => { expect(err.code).toEqual(209); expect(err.message).toEqual("Session token is expired."); Parse.User.logOut() // Logout to prevent polluting CLI with messages - .then(done()); + .then(done); }); }); }); diff --git a/spec/helper.js b/spec/helper.js index 472eea983e..a6ed46850c 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -11,7 +11,6 @@ var ParseServer = require('../src/index').ParseServer; var path = require('path'); var TestUtils = require('../src/index').TestUtils; var MongoStorageAdapter = require('../src/Adapters/Storage/Mongo/MongoStorageAdapter'); - const GridStoreAdapter = require('../src/Adapters/Files/GridStoreAdapter').GridStoreAdapter; const PostgresStorageAdapter = require('../src/Adapters/Storage/Postgres/PostgresStorageAdapter'); @@ -30,7 +29,6 @@ if (process.env.PARSE_SERVER_TEST_DB === 'postgres') { }) } - var port = 8378; let gridStoreAdapter = new GridStoreAdapter(mongoURI); diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 0b7394ac34..5b9470f340 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -1,4 +1,3 @@ - import MongoCollection from './MongoCollection'; function mongoFieldToParseSchemaField(type) { @@ -127,6 +126,12 @@ function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPe } } + // Legacy mongo adapter knows about the difference between password and _hashed_password. + // Future database adapters will only know about _hashed_password. + // Note: Parse Server will bring back password with injectDefaultSchema, so we don't need + // to add it here. + delete mongoObject._hashed_password; + return mongoObject; } diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 62f0703bdd..e2ade71c98 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -3,6 +3,15 @@ const pgp = require('pg-promise')(); const PostgresRelationDoesNotExistError = '42P01'; const PostgresDuplicateRelationError = '42P07'; +const parseTypeToPostgresType = type => { + switch (type.type) { + case 'String': return 'text'; + case 'Date': return 'timestamp'; + case 'Object': return 'jsonb'; + case 'Boolean': return 'boolean'; + default: throw `no type for ${JSON.stringify(type)} yet`; + } +}; export class PostgresStorageAdapter { // Private @@ -37,13 +46,20 @@ export class PostgresStorageAdapter { } createClass(className, schema) { - return this._client.query('CREATE TABLE $ ()', { className }) + let valuesArray = []; + let patternsArray = []; + Object.keys(schema.fields).forEach((fieldName, index) => { + valuesArray.push(fieldName); + valuesArray.push(parseTypeToPostgresType(schema.fields[fieldName])); + patternsArray.push(`$${index * 2 + 2}:name $${index * 2 + 3}:raw`); + }); + return this._client.query(`CREATE TABLE $1:name (${patternsArray.join(',')})`, [className, ...valuesArray]) .then(() => this._client.query('INSERT INTO "_SCHEMA" ("className", "schema", "isParseClass") VALUES ($, $, true)', { className, schema })) } addFieldIfNotExists(className, fieldName, type) { // TODO: Doing this in a transaction is probably a good idea. - return this._client.query('ALTER TABLE "GameScore" ADD COLUMN "score" double precision', { className, fieldName }) + return this._client.query('ALTER TABLE $ ADD COLUMN $ text', { className, fieldName }) .catch(error => { if (error.code === PostgresRelationDoesNotExistError) { return this.createClass(className, { fields: { [fieldName]: type } }) @@ -136,7 +152,16 @@ export class PostgresStorageAdapter { // TODO: remove the mongo format dependency createObject(className, schema, object) { - return this._client.query('INSERT INTO "GameScore" (score) VALUES ($)', { score: object.score }) + console.log(object); + let columnsArray = []; + let valuesArray = []; + Object.keys(object).forEach(fieldName => { + columnsArray.push(fieldName); + valuesArray.push(object[fieldName]); + }); + let columnsPattern = columnsArray.map((col, index) => `$${index + 2}~`).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]) .then(() => ({ ops: [object] })); } @@ -164,7 +189,7 @@ 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 }) + return this._client.query("SELECT * FROM $", { className }) } // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 0c5502157a..b8151526ae 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -220,6 +220,18 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => { return undefined; } +const convertSchemaToAdapterSchema = schema => { + schema = injectDefaultSchema(schema); + delete schema.fields.ACL; + + if (schema.className === '_User') { + delete schema.fields.password; + schema.fields._hashed_password = { type: 'String' }; + } + + return schema; +} + const injectDefaultSchema = schema => ({ className: schema.className, fields: { @@ -293,7 +305,7 @@ class SchemaController { return Promise.reject(validationError); } - return this._dbAdapter.createClass(className, { fields, classLevelPermissions }) + return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className })) .catch(error => { if (error && error.code === Parse.Error.DUPLICATE_VALUE) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); @@ -360,20 +372,15 @@ class SchemaController { // Returns a promise that resolves successfully to the new schema // object or fails with a reason. - // If 'freeze' is true, refuse to modify the schema. - enforceClassExists(className, freeze) { + enforceClassExists(className) { if (this.data[className]) { return Promise.resolve(this); } - if (freeze) { - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'schema is frozen, cannot add: ' + className); - } // We don't have this class. Update the schema - return this.addClassIfNotExists(className, {}).then(() => { + return this.addClassIfNotExists(className).then(() => { // The schema update succeeded. Reload the schema return this.reloadData(); - }, () => { + }, error => { // The schema update failed. This can be okay - it might // have failed because there's a race condition and a different // client is making the exact same schema update that we want. @@ -381,8 +388,12 @@ class SchemaController { return this.reloadData(); }).then(() => { // Ensure that the schema now validates - return this.enforceClassExists(className, true); - }, () => { + if (this.data[className]) { + return this; + } else { + throw new Parse.Error(Parse.Error.INVALID_JSON, `Failed to add ${className}`); + } + }, error => { // The schema still doesn't validate. Give up throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate'); }); From 2cafd6919f98ffd1e402c802174e64b9501431b2 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Fri, 10 Jun 2016 14:09:48 -0700 Subject: [PATCH 02/13] Passing postgres test with user --- .../Postgres/PostgresStorageAdapter.js | 49 ++++++++++++++++--- src/Controllers/DatabaseController.js | 2 +- src/Controllers/SchemaController.js | 3 ++ src/RestQuery.js | 6 +-- 4 files changed, 47 insertions(+), 13 deletions(-) diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index e2ade71c98..3006910add 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -9,6 +9,13 @@ const parseTypeToPostgresType = type => { case 'Date': return 'timestamp'; case 'Object': return 'jsonb'; case 'Boolean': return 'boolean'; + case 'Pointer': return 'char(10)'; + case 'Array': + if (type.contents && type.contents.type === 'String') { + return 'text[]'; + } else { + throw `no type for ${JSON.stringify(type)} yet`; + } default: throw `no type for ${JSON.stringify(type)} yet`; } }; @@ -50,7 +57,11 @@ export class PostgresStorageAdapter { let patternsArray = []; Object.keys(schema.fields).forEach((fieldName, index) => { valuesArray.push(fieldName); - valuesArray.push(parseTypeToPostgresType(schema.fields[fieldName])); + let parseType = schema.fields[fieldName]; + if (['_rperm', '_wperm'].includes(fieldName)) { + parseType.contents = { type: 'String' }; + } + valuesArray.push(parseTypeToPostgresType(parseType)); patternsArray.push(`$${index * 2 + 2}:name $${index * 2 + 3}:raw`); }); return this._client.query(`CREATE TABLE $1:name (${patternsArray.join(',')})`, [className, ...valuesArray]) @@ -128,7 +139,7 @@ export class PostgresStorageAdapter { } // Return a promise for all schemas known to this adapter, in Parse format. In case the - // schemas cannot be retrieved, returns a promise that rejects. Requirements for the + // schemas cannot be retrieved, returns a promise that rejects. Rquirements for the // rejection reason are TBD. getAllClasses() { return this._ensureSchemaCollectionExists() @@ -143,7 +154,7 @@ export class PostgresStorageAdapter { return this._client.query('SELECT * FROM "_SCHEMA" WHERE "className"=$', { className }) .then(result => { if (result.length === 1) { - return result; + return result[0]; } else { throw undefined; } @@ -152,24 +163,40 @@ export class PostgresStorageAdapter { // TODO: remove the mongo format dependency createObject(className, schema, object) { - console.log(object); let columnsArray = []; let valuesArray = []; Object.keys(object).forEach(fieldName => { columnsArray.push(fieldName); - valuesArray.push(object[fieldName]); + switch (schema.fields[fieldName].type) { + case 'Date': + valuesArray.push(object[fieldName].iso); + break; + case 'Pointer': + valuesArray.push(object[fieldName].objectId); + break; + default: + valuesArray.push(object[fieldName]); + break; + } }); - let columnsPattern = columnsArray.map((col, index) => `$${index + 2}~`).join(','); + 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]) - .then(() => ({ ops: [object] })); + .then(() => ({ ops: [object] })) } // Remove all objects that match the given Parse Query. // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. // If there is some other error, reject with INTERNAL_SERVER_ERROR. deleteObjectsByQuery(className, schema, query) { - return Promise.reject('Not implented yet.') + return this._client.query(`WITH deleted AS (DELETE FROM $ RETURNING *) SELECT count(*) FROM deleted`, { className }) + .then(result => { + if (result[0].count === 0) { + throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND, 'Object not found.'); + } else { + return result[0].count; + } + }); } // Apply the update to all objects that match the given Parse Query. @@ -190,6 +217,12 @@ 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 }) + .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 }; + }); + return object; + })) } // Create a unique index. Unique indexes on nullable fields are not allowed. Since we don't diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index a897dd2246..e9bf04ac38 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -410,7 +410,7 @@ DatabaseController.prototype.create = function(className, object, { acl } = {}) .then(() => this.handleRelationUpdates(className, null, object)) .then(() => schemaController.enforceClassExists(className)) .then(() => schemaController.getOneSchema(className, true)) - .then(schema => this.adapter.createObject(className, schema, object)) + .then(schema => this.adapter.createObject(className, SchemaController.convertSchemaToAdapterSchema(schema), object)) .then(result => sanitizeDatabaseResult(originalObject, result.ops[0])); }) }; diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b8151526ae..d18f9523c0 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -223,6 +223,8 @@ const fieldTypeIsInvalid = ({ type, targetClass }) => { const convertSchemaToAdapterSchema = schema => { schema = injectDefaultSchema(schema); delete schema.fields.ACL; + schema.fields._rperm = { type: 'Array' }; + schema.fields._wperm = { type: 'Array' }; if (schema.className === '_User') { delete schema.fields.password; @@ -837,4 +839,5 @@ export { buildMergedSchemaObject, systemClasses, defaultColumns, + convertSchemaToAdapterSchema, }; diff --git a/src/RestQuery.js b/src/RestQuery.js index 387052878e..349e4e14e8 100644 --- a/src/RestQuery.js +++ b/src/RestQuery.js @@ -526,16 +526,14 @@ function findPointers(object, path) { } if (typeof object !== 'object') { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'can only include pointer fields'); + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'can only include pointer fields'); } if (path.length == 0) { if (object.__type == 'Pointer') { return [object]; } - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'can only include pointer fields'); + throw new Parse.Error(Parse.Error.INVALID_QUERY, 'can only include pointer fields'); } var subobject = object[path[0]]; From 803b9be97aa5cf0f08f3ecadd25266d4b7a587a4 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Fri, 27 May 2016 19:41:09 -0700 Subject: [PATCH 03/13] Add unique indexing for username/email WIP Notes on how to upgrade to 2.3.0 safely index on unique-indexes: c454180 Revert "Log objects rather than JSON stringified objects (#1922)" reconfigure username/email tests Start dealing with test shittyness most tests passing Make specific server config for tests async Fix more tests Save callback to variable undo remove uses of _collection reorder some params reorder find() arguments finishsh touching up argument order Accept a database adapter as a parameter First passing test with postgres! Fix tests Setup travis sudo maybe? use postgres username reorder find() arguments Build objects with default fields correctly Don't tell adapter about ACL WIP Passing postgres test with user Fix up createdAt, updatedAt, nad _hashed_password handling --- 2.3.0.md | 2 +- spec/ParseAPI.spec.js | 100 ++++++++++++++++++- spec/Uniqueness.spec.js | 8 ++ src/Adapters/Storage/Mongo/MongoTransform.js | 22 ++-- src/Controllers/DatabaseController.js | 6 ++ 5 files changed, 121 insertions(+), 17 deletions(-) diff --git a/2.3.0.md b/2.3.0.md index 2528c290b6..fe58835498 100644 --- a/2.3.0.md +++ b/2.3.0.md @@ -77,6 +77,6 @@ coll.aggregate([ {$match: {count: {"$gt": 1}}}, {$project: {id: "$uniqueIds", username: "$_id", _id : 0} }, {$unwind: "$id" }, - {$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates collection. Remove this line to just output the list. + {$out: '_duplicates'} // Save the list of duplicates to a new, "_duplicates" collection. Remove this line to just output the list. ], {allowDiskUse:true}) ``` diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 64d49053ab..28a7d85677 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -207,7 +207,100 @@ describe('miscellaneous', function() { }); }); - it('succeed in logging in', function(done) { + it('ensure that email is uniquely indexed', done => { + let numCreated = 0; + let numFailed = 0; + + let user1 = new Parse.User(); + user1.setPassword('asdf'); + user1.setUsername('u1'); + user1.setEmail('dupe@dupe.dupe'); + let p1 = user1.signUp(); + p1.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); + + let user2 = new Parse.User(); + user2.setPassword('asdf'); + user2.setUsername('u2'); + user2.setEmail('dupe@dupe.dupe'); + let p2 = user2.signUp(); + p2.then(user => { + numCreated++; + expect(numCreated).toEqual(1); + }) + .catch(error => { + numFailed++; + expect(numFailed).toEqual(1); + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + }); + Parse.Promise.all([p1, p2]) + .then(() => { + fail('one of the users should not have been created'); + done(); + }) + .catch(done); + }); + + it('ensure that if people already have duplicate emails, they can still sign up new users', done => { + let config = new Config('test'); + // Remove existing data to clear out unique index + TestUtils.destroyAllDataPermanently() + .then(() => config.database.adapter.createObject('_User', requiredUserFields, { objectId: 'x', email: 'a@b.c' })) + .then(() => config.database.adapter.createObject('_User', requiredUserFields, { objectId: 'y', email: 'a@b.c' })) + .then(reconfigureServer) + .catch(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('qqq'); + user.setEmail('unique@unique.unique'); + return user.signUp().catch(fail); + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('www'); + user.setEmail('a@b.c'); + return user.signUp() + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); + 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.ensureUniqueness('_User', requiredUserFields, ['randomField']) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('1'); + user.setEmail('1@b.c'); + user.set('randomField', 'a'); + return user.signUp() + }) + .then(() => { + let user = new Parse.User(); + user.setPassword('asdf'); + user.setUsername('2'); + user.setEmail('2@b.c'); + user.set('randomField', 'a'); + return user.signUp() + }) + .catch(error => { + expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); + done(); + }); + }); + + fit('succeed in logging in', function(done) { createTestUser(function(u) { expect(typeof u.id).toEqual('string'); @@ -217,8 +310,9 @@ describe('miscellaneous', function() { expect(user.get('password')).toBeUndefined(); expect(user.getSessionToken()).not.toBeUndefined(); Parse.User.logOut().then(done); - }, error: function(error) { - fail(error); + }, error: error => { + fail(JSON.stringify(error)); + done(); } }); }, fail); diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js index 129ec07599..5892495465 100644 --- a/spec/Uniqueness.spec.js +++ b/spec/Uniqueness.spec.js @@ -100,4 +100,12 @@ describe('Uniqueness', function() { done(); }); }); + + it('adding a unique index to an existing field works even if it has nulls', done => { + + }); + + it('adding a unique index to an existing field doesnt prevent you from adding new documents with nulls', done => { + + }); }); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 4858a4364b..ce020278ff 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -197,20 +197,12 @@ function transformWhere(className, restWhere, schema) { return mongoWhere; } -const parseObjectKeyValueToMongoObjectKeyValue = (className, restKey, restValue, schema) => { +const parseObjectKeyValueToMongoObjectKeyValue = (restKey, restValue, schema) => { // Check if the schema is known since it's a built-in field. let transformedValue; let coercedToDate; switch(restKey) { case 'objectId': return {key: '_id', value: restValue}; - case 'createdAt': - transformedValue = transformTopLevelAtom(restValue); - coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue - return {key: '_created_at', value: coercedToDate}; - case 'updatedAt': - transformedValue = transformTopLevelAtom(restValue); - coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue - return {key: '_updated_at', value: coercedToDate}; case 'expiresAt': transformedValue = transformTopLevelAtom(restValue); coercedToDate = typeof transformedValue === 'string' ? new Date(transformedValue) : transformedValue @@ -271,8 +263,6 @@ const parseObjectKeyValueToMongoObjectKeyValue = (className, restKey, restValue, return {key: restKey, value}; } -// Main exposed method to create new objects. -// restCreate is the "create" clause in REST API form. const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => { if (className == '_User') { restCreate = transformAuthData(restCreate); @@ -281,7 +271,6 @@ const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => { let mongoCreate = {} for (let restKey in restCreate) { let { key, value } = parseObjectKeyValueToMongoObjectKeyValue( - className, restKey, restCreate[restKey], schema @@ -290,6 +279,13 @@ const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => { mongoCreate[key] = value; } } + + // Use the legacy mongo format for createdAt and updatedAt + mongoCreate._created_at = mongoCreate.createdAt.iso; + delete mongoCreate.createdAt; + mongoCreate._updated_at = mongoCreate.updatedAt.iso; + delete mongoCreate.updatedAt; + return mongoCreate; } @@ -735,7 +731,7 @@ const mongoObjectToParseObject = (className, mongoObject, schema) => { restObject['objectId'] = '' + mongoObject[key]; break; case '_hashed_password': - restObject['password'] = mongoObject[key]; + restObject._hashed_password = mongoObject[key]; break; case '_acl': case '_email_verify_token': diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index e9bf04ac38..bed0e154aa 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -159,6 +159,9 @@ const filterSensitiveData = (isMaster, aclGroup, className, object) => { return object; } + object.password = object._hashed_password; + delete object._hashed_password; + delete object.sessionToken; if (isMaster || (aclGroup.indexOf(object.objectId) > -1)) { @@ -400,6 +403,9 @@ DatabaseController.prototype.create = function(className, object, { acl } = {}) let originalObject = object; object = transformObjectACL(object); + object.createdAt = { iso: object.createdAt, __type: 'Date' }; + object.updatedAt = { iso: object.updatedAt, __type: 'Date' }; + var isMaster = acl === undefined; var aclGroup = acl || []; From 634d672ad1198dbf44efef5acb267c50c29c7127 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Sat, 11 Jun 2016 00:43:02 -0700 Subject: [PATCH 04/13] 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 28a7d85677..3ebbbb6ea5 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 55710aaa1f..24216ec61a 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 3006910add..70c510939d 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 bed0e154aa..221f6a7e37 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 9479991032..ebd7a2d2d6 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 45f0d7db74..4259529558 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(); }); } From 452b737be4fc182cf67ddcb01a829f1b7b484e17 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 13 Jun 2016 00:57:23 -0700 Subject: [PATCH 05/13] WIP --- spec/ParseAPI.spec.js | 47 +---------- spec/ParseQuery.spec.js | 2 +- src/Adapters/Storage/Mongo/MongoCollection.js | 2 +- src/Adapters/Storage/Mongo/MongoTransform.js | 12 ++- .../Postgres/PostgresStorageAdapter.js | 5 ++ src/Controllers/DatabaseController.js | 5 +- src/Controllers/SchemaController.js | 83 +++++++++---------- 7 files changed, 61 insertions(+), 95 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 3ebbbb6ea5..7895dae279 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -45,7 +45,7 @@ describe('miscellaneous', function() { }); }); - fit('create a valid parse user', function(done) { + it('create a valid parse user', function(done) { createTestUser(function(data) { expect(data.id).not.toBeUndefined(); expect(data.getSessionToken()).not.toBeUndefined(); @@ -90,7 +90,7 @@ describe('miscellaneous', function() { it('ensure that email is uniquely indexed', done => { let numFailed = 0; - + let numCreated = 0; let user1 = new Parse.User(); user1.setPassword('asdf'); user1.setUsername('u1'); @@ -207,47 +207,6 @@ describe('miscellaneous', function() { }); }); - it('ensure that email is uniquely indexed', done => { - let numCreated = 0; - let numFailed = 0; - - let user1 = new Parse.User(); - user1.setPassword('asdf'); - user1.setUsername('u1'); - user1.setEmail('dupe@dupe.dupe'); - let p1 = user1.signUp(); - p1.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); - - let user2 = new Parse.User(); - user2.setPassword('asdf'); - user2.setUsername('u2'); - user2.setEmail('dupe@dupe.dupe'); - let p2 = user2.signUp(); - p2.then(user => { - numCreated++; - expect(numCreated).toEqual(1); - }) - .catch(error => { - numFailed++; - expect(numFailed).toEqual(1); - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - }); - Parse.Promise.all([p1, p2]) - .then(() => { - fail('one of the users should not have been created'); - done(); - }) - .catch(done); - }); - it('ensure that if people already have duplicate emails, they can still sign up new users', done => { let config = new Config('test'); // Remove existing data to clear out unique index @@ -318,7 +277,7 @@ describe('miscellaneous', function() { }, fail); }); - fit('increment with a user object', function(done) { + it('increment with a user object', function(done) { createTestUser().then((user) => { user.increment('foo'); return user.save(); diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 036b106424..730e77249c 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1186,7 +1186,7 @@ describe('Parse.Query testing', () => { }); }); - it("regexes with invalid options fail", function(done) { + fit("regexes with invalid options fail", function(done) { var query = new Parse.Query(TestObject); query.matches("myString", "FootBall", "some invalid option"); query.find(expectError(Parse.Error.INVALID_QUERY, done)); diff --git a/src/Adapters/Storage/Mongo/MongoCollection.js b/src/Adapters/Storage/Mongo/MongoCollection.js index 889e640643..757be2091a 100644 --- a/src/Adapters/Storage/Mongo/MongoCollection.js +++ b/src/Adapters/Storage/Mongo/MongoCollection.js @@ -52,7 +52,7 @@ export default class MongoCollection { // If there is nothing that matches the query - does insert // Postgres Note: `INSERT ... ON CONFLICT UPDATE` that is available since 9.5. upsertOne(query, update) { - return this._mongoCollection.update(query, update, { upsert: true }); + return this._mongoCollection.update(query, update, { upsert: true }) } updateOne(query, update) { diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index ce020278ff..47a270b3b3 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -281,10 +281,14 @@ const parseObjectToMongoObjectForCreate = (className, restCreate, schema) => { } // Use the legacy mongo format for createdAt and updatedAt - mongoCreate._created_at = mongoCreate.createdAt.iso; - delete mongoCreate.createdAt; - mongoCreate._updated_at = mongoCreate.updatedAt.iso; - delete mongoCreate.updatedAt; + if (mongoCreate.createdAt) { + mongoCreate._created_at = new Date(mongoCreate.createdAt.iso || mongoCreate.createdAt); + delete mongoCreate.createdAt; + } + if (mongoCreate.updatedAt) { + mongoCreate._updated_at = new Date(mongoCreate.updatedAt.iso || mongoCreate.updatedAt); + delete mongoCreate.updatedAt; + } return mongoCreate; } diff --git a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js index 70c510939d..d1c299cb8e 100644 --- a/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js +++ b/src/Adapters/Storage/Postgres/PostgresStorageAdapter.js @@ -166,6 +166,11 @@ export class PostgresStorageAdapter { createObject(className, schema, object) { let columnsArray = []; let valuesArray = []; + console.log('creating'); + console.log(schema); + console.log(object); + console.log(className); + console.log(new Error().stack); Object.keys(object).forEach(fieldName => { columnsArray.push(fieldName); switch (schema.fields[fieldName].type) { diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 221f6a7e37..5c26889ccf 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -648,7 +648,10 @@ DatabaseController.prototype.find = function(className, query, { let classExists = true; return this.loadSchema() .then(schemaController => { - return schemaController.getOneSchema(className) + //Allow volatile classes if querying with Master (for _PushStatus) + //TODO: Move volatile classes concept into mongo adatper, postgres adapter shouldn't care + //that api.parse.com breaks when _PushStatus exists in mongo. + return schemaController.getOneSchema(className, isMaster) .catch(error => { // 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, diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index d18f9523c0..b5e5846955 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -97,7 +97,7 @@ const requiredColumns = Object.freeze({ const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus']); -const volatileClasses = Object.freeze(['_PushStatus']); +const volatileClasses = Object.freeze(['_PushStatus', '_Hooks', '_GlobalConfig']); // 10 alpha numberic chars + uppercase const userIdRegex = /^[a-zA-Z0-9]{10}$/; @@ -244,6 +244,14 @@ const injectDefaultSchema = schema => ({ classLevelPermissions: schema.classLevelPermissions, }) +const dbTypeMatchesObjectType = (dbType, objectType) => { + if (dbType.type !== objectType.type) return false; + if (dbType.targetClass !== objectType.targetClass) return false; + if (dbType === objectType.type) return true; + if (dbType.type === objectType.type) return true; + return false; +} + // Stores the entire schema of the app in a weird hybrid format somewhere between // the mongo format and the Parse format. Soon, this will all be Parse format. class SchemaController { @@ -358,7 +366,7 @@ class SchemaController { .then(() => { let promises = insertedFields.map(fieldName => { const type = submittedFields[fieldName]; - return this.validateField(className, fieldName, type); + return this.enforceFieldExists(className, fieldName, type); }); return Promise.all(promises); }) @@ -460,49 +468,36 @@ class SchemaController { // object if the provided className-fieldName-type tuple is valid. // The className must already be validated. // If 'freeze' is true, refuse to update the schema for this field. - validateField(className, fieldName, type, freeze) { - return this.reloadData().then(() => { - if (fieldName.indexOf(".") > 0) { - // subdocument key (x.y) => ok if x is of type 'object' - fieldName = fieldName.split(".")[ 0 ]; - type = 'Object'; - } + enforceFieldExists(className, fieldName, type, freeze) { + if (fieldName.indexOf(".") > 0) { + // subdocument key (x.y) => ok if x is of type 'object' + fieldName = fieldName.split(".")[ 0 ]; + type = 'Object'; + } + if (!fieldNameIsValid(fieldName)) { + throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + } - if (!fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); + // If someone tries to create a new field with null/undefined as the value, return; + if (!type) { + return Promise.resolve(this); + } + + return this.reloadData().then(() => { + let expectedType = this.getExpectedType(className, fieldName); + if (typeof type === 'string') { + type = { type }; } - let expected = this.data[className][fieldName]; - if (expected) { - expected = (expected === 'map' ? 'Object' : expected); - if (expected.type && type.type - && expected.type == type.type - && expected.targetClass == type.targetClass) { - return Promise.resolve(this); - } else if (expected == type || expected.type == type) { - return Promise.resolve(this); - } else { + if (expectedType) { + if (!dbTypeMatchesObjectType(expectedType, type)) { throw new Parse.Error( Parse.Error.INCORRECT_TYPE, - `schema mismatch for ${className}.${fieldName}; expected ${expected.type || expected} but got ${type}` + `schema mismatch for ${className}.${fieldName}; expected ${expectedType.type || expectedType} but got ${type}` ); } } - if (freeze) { - throw new Parse.Error(Parse.Error.INVALID_JSON, `schema is frozen, cannot add ${fieldName} field`); - } - - // We don't have this field, but if the value is null or undefined, - // we won't update the schema until we get a value with a type. - if (!type) { - return Promise.resolve(this); - } - - if (typeof type === 'string') { - type = { type }; - } - return this._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(() => { // The update succeeded. Reload the schema return this.reloadData(); @@ -513,11 +508,10 @@ class SchemaController { return this.reloadData(); }).then(() => { // Ensure that the schema now validates - return this.validateField(className, fieldName, type, true); - }, (error) => { - // The schema still doesn't validate. Give up - throw new Parse.Error(Parse.Error.INVALID_JSON, - 'schema key will not revalidate'); + if (!dbTypeMatchesObjectType(this.getExpectedType(className, fieldName), type)) { + throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`); + } + return this; }); }); } @@ -674,9 +668,10 @@ class SchemaController { // Returns the expected type for a className+key combination // or undefined if the schema is not set - getExpectedType(className, key) { + getExpectedType(className, fieldName) { if (this.data && this.data[className]) { - return this.data[className][key]; + const expectedType = this.data[className][fieldName] + return expectedType === 'map' ? 'Object' : expectedType; } return undefined; }; @@ -727,7 +722,7 @@ function buildMergedSchemaObject(existingFields, putRequest) { // validates this field once the schema loads. function thenValidateField(schemaPromise, className, key, type) { return schemaPromise.then((schema) => { - return schema.validateField(className, key, type); + return schema.enforceFieldExists(className, key, type); }); } From f796d69d19cb081cccfd32292a9420ad9a95667b Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 13 Jun 2016 01:14:26 -0700 Subject: [PATCH 06/13] Move $options validation into Parse Server --- spec/ParseQuery.spec.js | 2 +- src/Adapters/Storage/Mongo/MongoTransform.js | 8 +------- src/Controllers/DatabaseController.js | 7 +++++++ 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/spec/ParseQuery.spec.js b/spec/ParseQuery.spec.js index 730e77249c..036b106424 100644 --- a/spec/ParseQuery.spec.js +++ b/spec/ParseQuery.spec.js @@ -1186,7 +1186,7 @@ describe('Parse.Query testing', () => { }); }); - fit("regexes with invalid options fail", function(done) { + it("regexes with invalid options fail", function(done) { var query = new Parse.Query(TestObject); query.matches("myString", "FootBall", "some invalid option"); query.find(expectError(Parse.Error.INVALID_QUERY, done)); diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 47a270b3b3..1fff63787e 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -517,13 +517,7 @@ function transformConstraint(constraint, inArray) { break; case '$options': - var options = constraint[key]; - if (!answer['$regex'] || (typeof options !== 'string') - || !options.match(/^[imxs]+$/)) { - throw new Parse.Error(Parse.Error.INVALID_QUERY, - 'got a bad $options'); - } - answer[key] = options; + answer[key] = constraint[key]; break; case '$nearSphere': diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 5c26889ccf..bd28b990ba 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -67,6 +67,13 @@ const validateQuery = query => { } Object.keys(query).forEach(key => { + if (query[key].$regex) { + if (typeof query[key].$options === 'string') {g + if (!query[key].$options.match(/^[imxs]+$/)) { + throw new Parse.Error(Parse.Error.INVALID_QUERY, `Bad $options value for query: ${query[key].$options}`); + } + } + } if (!specialQuerykeys.includes(key) && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid key name: ${key}`); } From a69a88f3a439ed211b314ed700b15d7cd7744978 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 13 Jun 2016 12:57:20 -0700 Subject: [PATCH 07/13] Fix some stuff --- spec/MongoStorageAdapter.spec.js | 2 +- spec/ParseUser.spec.js | 76 ++++++++++--------- spec/PointerPermissions.spec.js | 4 +- spec/Schema.spec.js | 3 +- spec/Uniqueness.spec.js | 8 -- spec/helper.js | 17 ++--- .../Storage/Mongo/MongoStorageAdapter.js | 15 ++++ src/Controllers/DatabaseController.js | 6 +- src/Controllers/SchemaController.js | 17 +++++ 9 files changed, 86 insertions(+), 62 deletions(-) diff --git a/spec/MongoStorageAdapter.spec.js b/spec/MongoStorageAdapter.spec.js index 2ead5c1ebc..6c2666a4b0 100644 --- a/spec/MongoStorageAdapter.spec.js +++ b/spec/MongoStorageAdapter.spec.js @@ -49,7 +49,7 @@ describe('MongoStorageAdapter', () => { it('stores objectId in _id', done => { let adapter = new MongoStorageAdapter({ uri: databaseURI }); - adapter.createObject('Foo', {}, { objectId: 'abcde' }) + adapter.createObject('Foo', { fields: {} }, { objectId: 'abcde' }) .then(() => adapter._rawFind('Foo', {})) .then(results => { expect(results.length).toEqual(1); diff --git a/spec/ParseUser.spec.js b/spec/ParseUser.spec.js index 91a3b9a357..b3486177a8 100644 --- a/spec/ParseUser.spec.js +++ b/spec/ParseUser.spec.js @@ -10,6 +10,7 @@ var request = require('request'); var passwordCrypto = require('../src/password'); var Config = require('../src/Config'); +const rp = require('request-promise'); function verifyACL(user) { const ACL = user.getACL(); @@ -2131,7 +2132,7 @@ describe('Parse.User testing', () => { let database = new Config(Parse.applicationId).database; database.create('_User', { username: 'user', - password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie', + _hashed_password: '$2a$10$8/wZJyEuiEaobBBqzTG.jeY.XSFJd0rzaN//ososvEI4yLqI.4aie', _auth_data_facebook: null }, {}).then(() => { return new Promise((resolve, reject) => { @@ -2258,42 +2259,43 @@ describe('Parse.User testing', () => { }); it('should fail to become user with expired token', (done) => { - Parse.User.signUp("auser", "somepass", null, { - success: function(user) { - request.get({ - url: 'http://localhost:8378/1/classes/_Session', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - }, (error, response, body) => { - var id = body.results[0].objectId; - var expiresAt = new Date((new Date()).setYear(2015)); - var token = body.results[0].sessionToken; - request.put({ - url: "http://localhost:8378/1/classes/_Session/" + id, - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - body: { - expiresAt: { __type: "Date", iso: expiresAt.toISOString() }, - }, - }, (error, response, body) => { - Parse.User.become(token) - .then(() => { fail("Should not have succeded"); }) - .fail((err) => { - expect(err.code).toEqual(209); - expect(err.message).toEqual("Session token is expired."); - Parse.User.logOut() // Logout to prevent polluting CLI with messages - .then(done); - }); - }); - }); - } - }); + let token; + Parse.User.signUp("auser", "somepass", null) + .then(user => rp({ + method: 'GET', + url: 'http://localhost:8378/1/classes/_Session', + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + })) + .then(body => { + var id = body.results[0].objectId; + var expiresAt = new Date((new Date()).setYear(2015)); + token = body.results[0].sessionToken; + return rp({ + method: 'PUT', + url: "http://localhost:8378/1/classes/_Session/" + id, + json: true, + headers: { + 'X-Parse-Application-Id': 'test', + 'X-Parse-Master-Key': 'test', + }, + body: { + expiresAt: { __type: "Date", iso: expiresAt.toISOString() }, + }, + }) + }) + .then(() => Parse.User.become(token)) + .then(() => { + fail("Should not have succeded") + done(); + }, error => { + expect(error.code).toEqual(209); + expect(error.message).toEqual("Session token is expired."); + done(); + }) }); it('should not create extraneous session tokens', (done) => { diff --git a/spec/PointerPermissions.spec.js b/spec/PointerPermissions.spec.js index d78b5b6edf..4ac5f8c880 100644 --- a/spec/PointerPermissions.spec.js +++ b/spec/PointerPermissions.spec.js @@ -36,8 +36,8 @@ describe('Pointer Permissions', () => { expect(res.length).toBe(1); expect(res[0].id).toBe(obj.id); done(); - }).catch((err) => { - fail('Should not fail'); + }).catch(error => { + fail(JSON.stringify(error)); done(); }); }); diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index 0af93b2db3..b2b856eb37 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -238,6 +238,7 @@ describe('SchemaController', () => { }); Promise.all([p1,p2]) .catch(error => { + console.log(error); expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.message).toEqual('Class NewClass already exists.'); done(); @@ -693,7 +694,7 @@ describe('SchemaController', () => { objectId: { type: 'String' }, updatedAt: { type: 'Date' }, createdAt: { type: 'Date' }, - ACL: { type: 'ACL' } + ACL: { type: 'ACL' }, }; expect(dd(schema.data.NewClass, expectedSchema)).toEqual(undefined); done(); diff --git a/spec/Uniqueness.spec.js b/spec/Uniqueness.spec.js index 5892495465..129ec07599 100644 --- a/spec/Uniqueness.spec.js +++ b/spec/Uniqueness.spec.js @@ -100,12 +100,4 @@ describe('Uniqueness', function() { done(); }); }); - - it('adding a unique index to an existing field works even if it has nulls', done => { - - }); - - it('adding a unique index to an existing field doesnt prevent you from adding new documents with nulls', done => { - - }); }); diff --git a/spec/helper.js b/spec/helper.js index a6ed46850c..48b5b9ba8e 100644 --- a/spec/helper.js +++ b/spec/helper.js @@ -140,6 +140,12 @@ beforeEach(done => { }); afterEach(function(done) { + let afterLogOut = () => { + if (Object.keys(openConnections).length > 0) { + fail('There were open connections to the server left after the test finished'); + } + done(); + }; Parse.Cloud._removeAllHooks(); databaseAdapter.getAllClasses() .then(allSchemas => { @@ -157,16 +163,7 @@ afterEach(function(done) { }); }) .then(() => Parse.User.logOut()) - .then(() => { - if (Object.keys(openConnections).length > 0) { - fail('There were open connections to the server left after the test finished'); - } - done(); - }) - .catch(error => { - fail(JSON.stringify(error)); - done(); - }); + .then(afterLogOut, afterLogOut) }); var TestObject = Parse.Object.extend({ diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 24216ec61a..c9776c7ab5 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -34,6 +34,12 @@ const storageAdapterAllCollections = mongoAdapter => { }); } +const convertParseSchemaToMongoSchema = ({...schema}) => { + delete schema.fields._rperm; + delete schema.fields._wperm; + return schema; +} + export class MongoStorageAdapter { // Private _uri: string; @@ -97,6 +103,7 @@ export class MongoStorageAdapter { } createClass(className, schema) { + schema = convertParseSchemaToMongoSchema(schema); return this._schemaCollection() .then(schemaCollection => schemaCollection.addSchema(className, schema.fields, schema.classLevelPermissions)); } @@ -192,6 +199,7 @@ export class MongoStorageAdapter { // and should infer from the type. Or maybe does need the schema for validations. Or maybe needs // the schem only for the legacy mongo format. We'll figure that out later. createObject(className, schema, object) { + schema = convertParseSchemaToMongoSchema(schema); const mongoObject = parseObjectToMongoObjectForCreate(className, object, schema); return this._adaptiveCollection(className) .then(collection => collection.insertOne(mongoObject)) @@ -208,6 +216,7 @@ export class MongoStorageAdapter { // If no objects match, reject with OBJECT_NOT_FOUND. If objects are found and deleted, resolve with undefined. // If there is some other error, reject with INTERNAL_SERVER_ERROR. deleteObjectsByQuery(className, schema, query) { + schema = convertParseSchemaToMongoSchema(schema); return this._adaptiveCollection(className) .then(collection => { let mongoWhere = transformWhere(className, query, schema); @@ -225,6 +234,7 @@ export class MongoStorageAdapter { // Apply the update to all objects that match the given Parse Query. updateObjectsByQuery(className, schema, query, update) { + schema = convertParseSchemaToMongoSchema(schema); const mongoUpdate = transformUpdate(className, update, schema); const mongoWhere = transformWhere(className, query, schema); return this._adaptiveCollection(className) @@ -234,6 +244,7 @@ export class MongoStorageAdapter { // Atomically finds and updates an object based on query. // Return value not currently well specified. findOneAndUpdate(className, schema, query, update) { + schema = convertParseSchemaToMongoSchema(schema); const mongoUpdate = transformUpdate(className, update, schema); const mongoWhere = transformWhere(className, query, schema); return this._adaptiveCollection(className) @@ -243,6 +254,7 @@ export class MongoStorageAdapter { // Hopefully we can get rid of this. It's only used for config and hooks. upsertOneObject(className, schema, query, update) { + schema = convertParseSchemaToMongoSchema(schema); const mongoUpdate = transformUpdate(className, update, schema); const mongoWhere = transformWhere(className, query, schema); return this._adaptiveCollection(className) @@ -251,6 +263,7 @@ export class MongoStorageAdapter { // Executes a find. Accepts: className, query in Parse format, and { skip, limit, sort }. find(className, schema, query, { skip, limit, sort }) { + schema = convertParseSchemaToMongoSchema(schema); let mongoWhere = transformWhere(className, query, schema); let mongoSort = _.mapKeys(sort, (value, fieldName) => transformKey(className, fieldName, schema)); return this._adaptiveCollection(className) @@ -264,6 +277,7 @@ export class MongoStorageAdapter { // Way of determining if a field is nullable. Undefined doesn't count against uniqueness, // which is why we use sparse indexes. ensureUniqueness(className, schema, fieldNames) { + schema = convertParseSchemaToMongoSchema(schema); let indexCreationRequest = {}; let mongoFieldNames = fieldNames.map(fieldName => transformKey(className, fieldName, schema)); mongoFieldNames.forEach(fieldName => { @@ -287,6 +301,7 @@ export class MongoStorageAdapter { // Executs a count. count(className, schema, query) { + schema = convertParseSchemaToMongoSchema(schema); return this._adaptiveCollection(className) .then(collection => collection.count(transformWhere(className, query, schema))); } diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index bd28b990ba..65bea9137b 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -67,8 +67,8 @@ const validateQuery = query => { } Object.keys(query).forEach(key => { - if (query[key].$regex) { - if (typeof query[key].$options === 'string') {g + if (query && query[key] && query[key].$regex) { + if (typeof query[key].$options === 'string') { if (!query[key].$options.match(/^[imxs]+$/)) { throw new Parse.Error(Parse.Error.INVALID_QUERY, `Bad $options value for query: ${query[key].$options}`); } @@ -764,7 +764,7 @@ DatabaseController.prototype.deleteSchema = function(className) { }) .then(schema => { return this.collectionExists(className) - .then(exist => this.adapter.count(className)) + .then(exist => this.adapter.count(className, { fields: {} })) .then(count => { if (count > 0) { throw new Parse.Error(255, `Class ${className} is not empty, contains ${count} objects, cannot drop schema.`); diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index b5e5846955..77e22f8ac8 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -234,6 +234,20 @@ const convertSchemaToAdapterSchema = schema => { return schema; } +const convertAdapterSchemaToParseSchema = ({...schema}) => { + delete schema.fields._rperm; + delete schema.fields._wperm; + + schema.fields.ACL = { type: 'ACL' }; + + if (schema.className === '_User') { + delete schema.fields._hashed_password; + schema.fields.password = { type: 'String' }; + } + + return schema; +} + const injectDefaultSchema = schema => ({ className: schema.className, fields: { @@ -316,6 +330,7 @@ class SchemaController { } return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className })) + .then(convertAdapterSchemaToParseSchema) .catch(error => { if (error && error.code === Parse.Error.DUPLICATE_VALUE) { throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); @@ -342,6 +357,8 @@ class SchemaController { } }); + delete existingFields._rperm; + delete existingFields._wperm; let newSchema = buildMergedSchemaObject(existingFields, submittedFields); let validationError = this.validateSchemaData(className, newSchema, classLevelPermissions); if (validationError) { From 510a890a03600f9d20edd9a90ec19ac8c228e57a Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 13 Jun 2016 13:14:29 -0700 Subject: [PATCH 08/13] centralize mongo schema handling logic --- src/Adapters/Storage/Mongo/MongoSchemaCollection.js | 6 ------ src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 9 +++++++++ 2 files changed, 9 insertions(+), 6 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 5b9470f340..274d8c9423 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -126,12 +126,6 @@ function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPe } } - // Legacy mongo adapter knows about the difference between password and _hashed_password. - // Future database adapters will only know about _hashed_password. - // Note: Parse Server will bring back password with injectDefaultSchema, so we don't need - // to add it here. - delete mongoObject._hashed_password; - return mongoObject; } diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index c9776c7ab5..6082952fbb 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -37,6 +37,15 @@ const storageAdapterAllCollections = mongoAdapter => { const convertParseSchemaToMongoSchema = ({...schema}) => { delete schema.fields._rperm; delete schema.fields._wperm; + + if (schema.className === '_User') { + // Legacy mongo adapter knows about the difference between password and _hashed_password. + // Future database adapters will only know about _hashed_password. + // Note: Parse Server will bring back password with injectDefaultSchema, so we don't need + // to add _hashed_password back ever. + delete schema.fields._hashed_password; + } + return schema; } From f23d80ec9a1981438cef1fbe42d79044ae266d69 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 13 Jun 2016 17:22:18 -0700 Subject: [PATCH 09/13] Ignore coverage requirements for postgres adapter until I can figure out how to run coverage on it. --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 11d1f176b2..5a6de43404 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", "test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest", "posttest": "./node_modules/.bin/mongodb-runner stop", - "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test", + "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' -x */PostgresStorageAdapter.js npm test", "start": "node ./bin/parse-server", "prepublish": "npm run build" }, From 81306585834150ccaa0107fe6291f68278978db9 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Wed, 15 Jun 2016 15:01:21 -0700 Subject: [PATCH 10/13] Fix purge --- src/Adapters/Storage/Mongo/MongoStorageAdapter.js | 2 +- src/Controllers/DatabaseController.js | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 6082952fbb..b24ed6b92d 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -201,7 +201,7 @@ export class MongoStorageAdapter { // undefined as the reason. getClass(className) { return this._schemaCollection() - .then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className)); + .then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className)) } // TODO: As yet not particularly well specified. Creates an object. Maybe shouldn't even need the schema, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 65bea9137b..9e3c6e3858 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -100,12 +100,8 @@ DatabaseController.prototype.collectionExists = function(className) { DatabaseController.prototype.purgeCollection = function(className) { return this.loadSchema() - .then((schema) => { - schema.getOneSchema(className) - }) - .then((schema) => { - this.adapter.deleteObjectsByQuery(className, {}, schema); - }); + .then(schemaController => schemaController.getOneSchema(className)) + .then(schema => this.adapter.deleteObjectsByQuery(className, schema, {})); }; DatabaseController.prototype.validateClassName = function(className) { From 9ed3fcc5365fd5e528f2f2b2875c3795aea2b3e8 Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 15 Jun 2016 17:23:29 -0700 Subject: [PATCH 11/13] Remove duplicate test --- spec/ParseAPI.spec.js | 52 ------------------------------------------- 1 file changed, 52 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 7895dae279..b82035b15a 100644 --- a/spec/ParseAPI.spec.js +++ b/spec/ParseAPI.spec.js @@ -207,58 +207,6 @@ describe('miscellaneous', function() { }); }); - it('ensure that if people already have duplicate emails, they can still sign up new users', done => { - let config = new Config('test'); - // Remove existing data to clear out unique index - TestUtils.destroyAllDataPermanently() - .then(() => config.database.adapter.createObject('_User', requiredUserFields, { objectId: 'x', email: 'a@b.c' })) - .then(() => config.database.adapter.createObject('_User', requiredUserFields, { objectId: 'y', email: 'a@b.c' })) - .then(reconfigureServer) - .catch(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('qqq'); - user.setEmail('unique@unique.unique'); - return user.signUp().catch(fail); - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('www'); - user.setEmail('a@b.c'); - return user.signUp() - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.EMAIL_TAKEN); - 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.ensureUniqueness('_User', requiredUserFields, ['randomField']) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('1'); - user.setEmail('1@b.c'); - user.set('randomField', 'a'); - return user.signUp() - }) - .then(() => { - let user = new Parse.User(); - user.setPassword('asdf'); - user.setUsername('2'); - user.setEmail('2@b.c'); - user.set('randomField', 'a'); - return user.signUp() - }) - .catch(error => { - expect(error.code).toEqual(Parse.Error.DUPLICATE_VALUE); - done(); - }); - }); - it('succeed in logging in', function(done) { createTestUser(function(u) { expect(typeof u.id).toEqual('string'); From f7f5bab75dd176124e0ff180caf52f573c9c76c5 Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 15 Jun 2016 17:24:00 -0700 Subject: [PATCH 12/13] Remove attempt to fix tests --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5a6de43404..11d1f176b2 100644 --- a/package.json +++ b/package.json @@ -71,7 +71,7 @@ "test": "cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node $COVERAGE_OPTION ./node_modules/jasmine/bin/jasmine.js", "test:win": "npm run pretest && cross-env NODE_ENV=test TESTING=1 ./node_modules/.bin/babel-node ./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/** ./node_modules/jasmine/bin/jasmine.js && npm run posttest", "posttest": "./node_modules/.bin/mongodb-runner stop", - "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' -x */PostgresStorageAdapter.js npm test", + "coverage": "cross-env COVERAGE_OPTION='./node_modules/babel-istanbul/lib/cli.js cover -x **/spec/**' npm test", "start": "node ./bin/parse-server", "prepublish": "npm run build" }, From 726c866a4d6953711edaca5199dd689372e30d36 Mon Sep 17 00:00:00 2001 From: Drew Date: Wed, 15 Jun 2016 17:25:17 -0700 Subject: [PATCH 13/13] Remove console.log --- spec/Schema.spec.js | 1 - 1 file changed, 1 deletion(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index b2b856eb37..089ab91383 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -238,7 +238,6 @@ describe('SchemaController', () => { }); Promise.all([p1,p2]) .catch(error => { - console.log(error); expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); expect(error.message).toEqual('Class NewClass already exists.'); done();