From 452b737be4fc182cf67ddcb01a829f1b7b484e17 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 13 Jun 2016 00:57:23 -0700 Subject: [PATCH] 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 | 91 +++++++++---------- 7 files changed, 65 insertions(+), 99 deletions(-) diff --git a/spec/ParseAPI.spec.js b/spec/ParseAPI.spec.js index 3ebbbb6e..7895dae2 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 036b1064..730e7724 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 889e6406..757be209 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 ce020278..47a270b3 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 70c51093..d1c299cb 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 221f6a7e..5c26889c 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 d18f9523..b5e58469 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) { + 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 someone tries to create a new field with null/undefined as the value, return; + if (!type) { + return Promise.resolve(this); + } + 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'; - } - - if (!fieldNameIsValid(fieldName)) { - throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`); - } - - 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 { - throw new Parse.Error( - Parse.Error.INCORRECT_TYPE, - `schema mismatch for ${className}.${fieldName}; expected ${expected.type || expected} 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); - } - + let expectedType = this.getExpectedType(className, fieldName); if (typeof type === 'string') { type = { type }; } + if (expectedType) { + if (!dbTypeMatchesObjectType(expectedType, type)) { + throw new Parse.Error( + Parse.Error.INCORRECT_TYPE, + `schema mismatch for ${className}.${fieldName}; expected ${expectedType.type || expectedType} but got ${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); }); }