diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 7bba6c9b..64d49053 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 3d1e45d6..7663e9a4 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 1131e95c..91a3b9a3 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 472eea98..a6ed4685 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 0b7394ac..5b9470f3 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 62f0703b..e2ade71c 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 0c550215..b8151526 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'); });