From 15969e4cc608ea5e476a08388451e1757409218f Mon Sep 17 00:00:00 2001 From: = Date: Sun, 21 Jul 2019 22:48:23 -0700 Subject: [PATCH 01/12] Add field options to mongo schema metadata --- .../Storage/Mongo/MongoSchemaCollection.js | 49 ++++++++++++++++--- .../Storage/Mongo/MongoStorageAdapter.js | 13 ++++- 2 files changed, 54 insertions(+), 8 deletions(-) diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index 160bccf450..c12deef353 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -46,6 +46,17 @@ function mongoSchemaFieldsToParseSchemaFields(schema) { ); var response = fieldNames.reduce((obj, fieldName) => { obj[fieldName] = mongoFieldToParseSchemaField(schema[fieldName]); + if ( + schema._metadata && + schema._metadata.fields_options && + schema._metadata.field_options[fieldName] + ) { + obj[fieldName] = Object.assign( + {}, + obj[fieldName], + schema._metadata.field_options[fieldName] + ); + } return obj; }, {}); response.ACL = { type: 'ACL' }; @@ -210,7 +221,7 @@ class MongoSchemaCollection { // Support additional types that Mongo doesn't, like Money, or something. // TODO: don't spend an extra query on finding the schema if the type we are trying to add isn't a GeoPoint. - addFieldIfNotExists(className: string, fieldName: string, type: string) { + addFieldIfNotExists(className: string, fieldName: string, fieldType: string) { return this._fetchOneSchemaFrom_SCHEMA(className) .then( schema => { @@ -219,7 +230,7 @@ class MongoSchemaCollection { return; } // The schema exists. Check for existing GeoPoints. - if (type.type === 'GeoPoint') { + if (fieldType.type === 'GeoPoint') { // Make sure there are not other geopoint fields if ( Object.keys(schema.fields).some( @@ -245,13 +256,37 @@ class MongoSchemaCollection { } ) .then(() => { + const { type, targetClass, ...fieldOptions } = fieldType; // We use $exists and $set to avoid overwriting the field type if it // already exists. (it could have added inbetween the last query and the update) - return this.upsertSchema( - className, - { [fieldName]: { $exists: false } }, - { $set: { [fieldName]: parseFieldTypeToMongoFieldType(type) } } - ); + if (fieldOptions && Object.keys(fieldOptions).length > 0) { + return this.upsertSchema( + className, + { [fieldName]: { $exists: false } }, + { + $set: { + [fieldName]: parseFieldTypeToMongoFieldType({ + type, + targetClass, + }), + [`_metadata.fields_options.${fieldName}`]: fieldOptions, + }, + } + ); + } else { + return this.upsertSchema( + className, + { [fieldName]: { $exists: false } }, + { + $set: { + [fieldName]: parseFieldTypeToMongoFieldType({ + type, + targetClass, + }), + }, + } + ); + } }); } } diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index 33282ce2b9..14128bbc54 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -84,9 +84,19 @@ const mongoSchemaFromFieldsAndClassNameAndCLP = ( }; for (const fieldName in fields) { + const { type, targetClass, ...fieldOptions } = fields[fieldName]; mongoObject[ fieldName - ] = MongoSchemaCollection.parseFieldTypeToMongoFieldType(fields[fieldName]); + ] = MongoSchemaCollection.parseFieldTypeToMongoFieldType({ + type, + targetClass, + }); + if (fieldOptions && Object.keys(fieldOptions).length > 0) { + mongoObject._metadata = mongoObject._metadata || {}; + mongoObject._metadata.fields_options = + mongoObject._metadata.fields_options || {}; + mongoObject._metadata.fields_options[fieldName] = fieldOptions; + } } if (typeof classLevelPermissions !== 'undefined') { @@ -425,6 +435,7 @@ export class MongoStorageAdapter implements StorageAdapter { const schemaUpdate = { $unset: {} }; fieldNames.forEach(name => { schemaUpdate['$unset'][name] = null; + schemaUpdate['$unset'][`_metadata.fields_options.${name}`] = null; }); return this._adaptiveCollection(className) From bbebcf70a50e231a3371afc31bdf8297d79e104b Mon Sep 17 00:00:00 2001 From: = Date: Sun, 21 Jul 2019 23:06:16 -0700 Subject: [PATCH 02/12] Add/fix test with fields options --- spec/schemas.spec.js | 66 +++++++++++++++++++ .../Storage/Mongo/MongoSchemaCollection.js | 4 +- 2 files changed, 68 insertions(+), 2 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 4e66426623..9201766772 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -737,6 +737,72 @@ describe('schemas', () => { }); }); + fit('lets you add fields with options', done => { + request({ + url: 'http://localhost:8378/1/schemas/NewClass', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: {}, + }).then(() => { + request({ + method: 'PUT', + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newField: { + type: 'String', + required: true, + defaultValue: 'some value', + }, + }, + }, + }).then(response => { + expect( + dd(response.data, { + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + objectId: { type: 'String' }, + updatedAt: { type: 'Date' }, + newField: { + type: 'String', + required: true, + defaultValue: 'some value', + }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }) + ).toEqual(undefined); + request({ + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + }).then(response => { + expect(response.data).toEqual({ + className: 'NewClass', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + newField: { + type: 'String', + required: true, + defaultValue: 'some value', + }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }); + done(); + }); + }); + }); + }); + it('lets you add fields to system schema', done => { request({ method: 'POST', diff --git a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js index c12deef353..338cc921c5 100644 --- a/src/Adapters/Storage/Mongo/MongoSchemaCollection.js +++ b/src/Adapters/Storage/Mongo/MongoSchemaCollection.js @@ -49,12 +49,12 @@ function mongoSchemaFieldsToParseSchemaFields(schema) { if ( schema._metadata && schema._metadata.fields_options && - schema._metadata.field_options[fieldName] + schema._metadata.fields_options[fieldName] ) { obj[fieldName] = Object.assign( {}, obj[fieldName], - schema._metadata.field_options[fieldName] + schema._metadata.fields_options[fieldName] ); } return obj; From 984ddbc5de45f9bd68824b5118a9e78644dd655a Mon Sep 17 00:00:00 2001 From: = Date: Sun, 21 Jul 2019 23:18:42 -0700 Subject: [PATCH 03/12] Add required validation failing test --- spec/schemas.spec.js | 58 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 57 insertions(+), 1 deletion(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 9201766772..fbcebf8c53 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -737,7 +737,7 @@ describe('schemas', () => { }); }); - fit('lets you add fields with options', done => { + it('lets you add fields with options', done => { request({ url: 'http://localhost:8378/1/schemas/NewClass', method: 'POST', @@ -803,6 +803,62 @@ describe('schemas', () => { }); }); + fit('should validate required fields', done => { + request({ + url: 'http://localhost:8378/1/schemas/NewClass', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: {}, + }).then(() => { + request({ + method: 'PUT', + url: 'http://localhost:8378/1/schemas/NewClass', + headers: masterKeyHeaders, + json: true, + body: { + fields: { + newRequiredField: { + type: 'String', + required: true, + }, + newRequiredFieldWithDefaultValue: { + type: 'String', + required: true, + defaultValue: 'some value', + }, + newNotRequiredField: { + type: 'String', + required: false, + }, + newNotRequiredFieldWithDefaultValue: { + type: 'String', + required: false, + defaultValue: 'some value', + }, + newRegularFieldWithDefaultValue: { + type: 'String', + defaultValue: 'some value', + }, + newRegularField: { + type: 'String', + }, + }, + }, + }).then(async () => { + const obj = new Parse.Object('NewClass'); + try { + await obj.save(); + fail('Should fail'); + } catch (e) { + expect(e.code).toEqual(142); + expect(e.message).toEqual('newRequiredField is required'); + } + done(); + }); + }); + }); + it('lets you add fields to system schema', done => { request({ method: 'POST', From 9ad03872e3e069c75f388042286993af0f5be340 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 22 Jul 2019 00:17:36 -0700 Subject: [PATCH 04/12] Add more tests --- spec/schemas.spec.js | 90 +++++++++++++++++++++++++++++++++++++++++++- src/RestWrite.js | 57 ++++++++++++++++++++++++---- 2 files changed, 137 insertions(+), 10 deletions(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index fbcebf8c53..7ffdfe8955 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -803,7 +803,7 @@ describe('schemas', () => { }); }); - fit('should validate required fields', done => { + it('should validate required fields', done => { request({ url: 'http://localhost:8378/1/schemas/NewClass', method: 'POST', @@ -846,7 +846,7 @@ describe('schemas', () => { }, }, }).then(async () => { - const obj = new Parse.Object('NewClass'); + let obj = new Parse.Object('NewClass'); try { await obj.save(); fail('Should fail'); @@ -854,6 +854,92 @@ describe('schemas', () => { expect(e.code).toEqual(142); expect(e.message).toEqual('newRequiredField is required'); } + obj.set('newRequiredField', 'some value'); + await obj.save(); + expect(obj.get('newRequiredField')).toEqual('some value'); + expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual( + 'some value' + ); + expect(obj.get('newNotRequiredField')).toEqual(undefined); + expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual( + 'some value' + ); + expect(obj.get('newRegularField')).toEqual(undefined); + obj.set('newRequiredField', null); + try { + await obj.save(); + fail('Should fail'); + } catch (e) { + expect(e.code).toEqual(142); + expect(e.message).toEqual('newRequiredField is required'); + } + obj.unset('newRequiredField'); + try { + await obj.save(); + fail('Should fail'); + } catch (e) { + expect(e.code).toEqual(142); + expect(e.message).toEqual('newRequiredField is required'); + } + obj.set('newRequiredField', 'some value2'); + await obj.save(); + expect(obj.get('newRequiredField')).toEqual('some value2'); + expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual( + 'some value' + ); + expect(obj.get('newNotRequiredField')).toEqual(undefined); + expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual( + 'some value' + ); + expect(obj.get('newRegularField')).toEqual(undefined); + obj.unset('newRequiredFieldWithDefaultValue'); + try { + await obj.save(); + fail('Should fail'); + } catch (e) { + expect(e.code).toEqual(142); + expect(e.message).toEqual( + 'newRequiredFieldWithDefaultValue is required' + ); + } + obj.set('newRequiredFieldWithDefaultValue', ''); + try { + await obj.save(); + fail('Should fail'); + } catch (e) { + expect(e.code).toEqual(142); + expect(e.message).toEqual( + 'newRequiredFieldWithDefaultValue is required' + ); + } + obj.set('newRequiredFieldWithDefaultValue', 'some value2'); + obj.set('newNotRequiredField', ''); + obj.set('newNotRequiredFieldWithDefaultValue', null); + obj.unset('newRegularField'); + await obj.save(); + expect(obj.get('newRequiredField')).toEqual('some value2'); + expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual( + 'some value2' + ); + expect(obj.get('newNotRequiredField')).toEqual(''); + expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual(null); + expect(obj.get('newRegularField')).toEqual(undefined); + obj = new Parse.Object('NewClass'); + obj.set('newRequiredField', 'some value3'); + obj.set('newRequiredFieldWithDefaultValue', 'some value3'); + obj.set('newNotRequiredField', 'some value3'); + obj.set('newNotRequiredFieldWithDefaultValue', 'some value3'); + obj.set('newRegularField', 'some value3'); + await obj.save(); + expect(obj.get('newRequiredField')).toEqual('some value3'); + expect(obj.get('newRequiredFieldWithDefaultValue')).toEqual( + 'some value3' + ); + expect(obj.get('newNotRequiredField')).toEqual('some value3'); + expect(obj.get('newNotRequiredFieldWithDefaultValue')).toEqual( + 'some value3' + ); + expect(obj.get('newRegularField')).toEqual('some value3'); done(); }); }); diff --git a/src/RestWrite.js b/src/RestWrite.js index 3fea6043de..cd19e6309b 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -323,16 +323,57 @@ RestWrite.prototype.runBeforeLoginTrigger = async function(userData) { RestWrite.prototype.setRequiredFieldsIfNeeded = function() { if (this.data) { - // Add default fields - this.data.updatedAt = this.updatedAt; - if (!this.query) { - this.data.createdAt = this.updatedAt; + return this.validSchemaController.getAllClasses().then(allClasses => { + const schema = allClasses.find( + oneClass => oneClass.className === this.className + ); - // Only assign new objectId if we are creating new object - if (!this.data.objectId) { - this.data.objectId = cryptoUtils.newObjectId(this.config.objectIdSize); + const setRequiredFieldIfNeeded = (fieldName, setDefault) => { + if ( + this.data[fieldName] === undefined || + this.data[fieldName] === null || + this.data[fieldName] === '' || + (typeof this.data[fieldName] === 'object' && + this.data[fieldName].__op === 'Delete') + ) { + if (setDefault && schema.fields[fieldName].defaultValue) { + this.data[fieldName] = schema.fields[fieldName].defaultValue; + this.storage.fieldsChangedByTrigger = + this.storage.fieldsChangedByTrigger || []; + if (this.storage.fieldsChangedByTrigger.indexOf(fieldName) < 0) { + this.storage.fieldsChangedByTrigger.push(fieldName); + } + } else if (schema.fields[fieldName].required === true) { + throw new Parse.Error( + Parse.Error.VALIDATION_ERROR, + `${fieldName} is required` + ); + } + } + }; + + // Add default fields + this.data.updatedAt = this.updatedAt; + if (!this.query) { + this.data.createdAt = this.updatedAt; + + // Only assign new objectId if we are creating new object + if (!this.data.objectId) { + this.data.objectId = cryptoUtils.newObjectId( + this.config.objectIdSize + ); + } + if (schema) { + Object.keys(schema.fields).forEach(fieldName => { + setRequiredFieldIfNeeded(fieldName, true); + }); + } + } else if (schema) { + Object.keys(this.data).forEach(fieldName => { + setRequiredFieldIfNeeded(fieldName, false); + }); } - } + }); } return Promise.resolve(); }; From 84033fb1c04cc126ef99764fb8b8fffcfa9ac396 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 22 Jul 2019 00:36:41 -0700 Subject: [PATCH 05/12] Only set default value if field is undefined --- src/RestWrite.js | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index cd19e6309b..33f9313354 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -336,14 +336,22 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function() { (typeof this.data[fieldName] === 'object' && this.data[fieldName].__op === 'Delete') ) { - if (setDefault && schema.fields[fieldName].defaultValue) { + if ( + setDefault && + schema.fields[fieldName] && + schema.fields[fieldName].defaultValue && + this.data[fieldName] === undefined + ) { this.data[fieldName] = schema.fields[fieldName].defaultValue; this.storage.fieldsChangedByTrigger = this.storage.fieldsChangedByTrigger || []; if (this.storage.fieldsChangedByTrigger.indexOf(fieldName) < 0) { this.storage.fieldsChangedByTrigger.push(fieldName); } - } else if (schema.fields[fieldName].required === true) { + } else if ( + schema.fields[fieldName] && + schema.fields[fieldName].required === true + ) { throw new Parse.Error( Parse.Error.VALIDATION_ERROR, `${fieldName} is required` From 39e0e81493a5b99b27d88d8a4516a4b7c51e4dd9 Mon Sep 17 00:00:00 2001 From: = Date: Mon, 22 Jul 2019 01:34:34 -0700 Subject: [PATCH 06/12] Fix redis test --- spec/RedisCacheAdapter.spec.js | 18 +++--- src/RestWrite.js | 100 ++++++++++++++++----------------- 2 files changed, 58 insertions(+), 60 deletions(-) diff --git a/spec/RedisCacheAdapter.spec.js b/spec/RedisCacheAdapter.spec.js index 4f90022870..3b1c78af04 100644 --- a/spec/RedisCacheAdapter.spec.js +++ b/spec/RedisCacheAdapter.spec.js @@ -188,7 +188,7 @@ describe_only(() => { const object = new TestObject(); object.set('foo', 'bar'); await object.save(); - expect(getSpy.calls.count()).toBe(2); + expect(getSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(2); }); @@ -201,7 +201,7 @@ describe_only(() => { booleanField: true, }); await container.save(); - expect(getSpy.calls.count()).toBe(2); + expect(getSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(2); }); @@ -215,7 +215,7 @@ describe_only(() => { object.set('foo', 'barz'); await object.save(); - expect(getSpy.calls.count()).toBe(2); + expect(getSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(0); }); @@ -233,7 +233,7 @@ describe_only(() => { objects.push(object); } await Parse.Object.saveAll(objects); - expect(getSpy.calls.count()).toBe(11); + expect(getSpy.calls.count()).toBe(21); expect(putSpy.calls.count()).toBe(10); getSpy.calls.reset(); @@ -258,7 +258,7 @@ describe_only(() => { objects.push(object); } await Parse.Object.saveAll(objects, { batchSize: 5 }); - expect(getSpy.calls.count()).toBe(12); + expect(getSpy.calls.count()).toBe(22); expect(putSpy.calls.count()).toBe(5); getSpy.calls.reset(); @@ -279,7 +279,7 @@ describe_only(() => { object.set('new', 'barz'); await object.save(); - expect(getSpy.calls.count()).toBe(2); + expect(getSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(1); }); @@ -299,7 +299,7 @@ describe_only(() => { booleanField: true, }); await object.save(); - expect(getSpy.calls.count()).toBe(2); + expect(getSpy.calls.count()).toBe(3); expect(putSpy.calls.count()).toBe(1); }); @@ -309,7 +309,7 @@ describe_only(() => { user.setPassword('testing'); await user.signUp(); - expect(getSpy.calls.count()).toBe(6); + expect(getSpy.calls.count()).toBe(8); expect(putSpy.calls.count()).toBe(1); }); @@ -326,7 +326,7 @@ describe_only(() => { object.set('foo', 'bar'); await object.save(); - expect(getSpy.calls.count()).toBe(3); + expect(getSpy.calls.count()).toBe(4); expect(putSpy.calls.count()).toBe(1); getSpy.calls.reset(); diff --git a/src/RestWrite.js b/src/RestWrite.js index 33f9313354..cc642d044e 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -323,65 +323,63 @@ RestWrite.prototype.runBeforeLoginTrigger = async function(userData) { RestWrite.prototype.setRequiredFieldsIfNeeded = function() { if (this.data) { - return this.validSchemaController.getAllClasses().then(allClasses => { - const schema = allClasses.find( - oneClass => oneClass.className === this.className - ); - - const setRequiredFieldIfNeeded = (fieldName, setDefault) => { - if ( - this.data[fieldName] === undefined || - this.data[fieldName] === null || - this.data[fieldName] === '' || - (typeof this.data[fieldName] === 'object' && - this.data[fieldName].__op === 'Delete') - ) { + return this.validSchemaController + .getOneSchema(this.className) + .then(schema => { + const setRequiredFieldIfNeeded = (fieldName, setDefault) => { if ( - setDefault && - schema.fields[fieldName] && - schema.fields[fieldName].defaultValue && - this.data[fieldName] === undefined + this.data[fieldName] === undefined || + this.data[fieldName] === null || + this.data[fieldName] === '' || + (typeof this.data[fieldName] === 'object' && + this.data[fieldName].__op === 'Delete') ) { - this.data[fieldName] = schema.fields[fieldName].defaultValue; - this.storage.fieldsChangedByTrigger = - this.storage.fieldsChangedByTrigger || []; - if (this.storage.fieldsChangedByTrigger.indexOf(fieldName) < 0) { - this.storage.fieldsChangedByTrigger.push(fieldName); + if ( + setDefault && + schema.fields[fieldName] && + schema.fields[fieldName].defaultValue && + this.data[fieldName] === undefined + ) { + this.data[fieldName] = schema.fields[fieldName].defaultValue; + this.storage.fieldsChangedByTrigger = + this.storage.fieldsChangedByTrigger || []; + if (this.storage.fieldsChangedByTrigger.indexOf(fieldName) < 0) { + this.storage.fieldsChangedByTrigger.push(fieldName); + } + } else if ( + schema.fields[fieldName] && + schema.fields[fieldName].required === true + ) { + throw new Parse.Error( + Parse.Error.VALIDATION_ERROR, + `${fieldName} is required` + ); } - } else if ( - schema.fields[fieldName] && - schema.fields[fieldName].required === true - ) { - throw new Parse.Error( - Parse.Error.VALIDATION_ERROR, - `${fieldName} is required` - ); } - } - }; + }; - // Add default fields - this.data.updatedAt = this.updatedAt; - if (!this.query) { - this.data.createdAt = this.updatedAt; + // Add default fields + this.data.updatedAt = this.updatedAt; + if (!this.query) { + this.data.createdAt = this.updatedAt; - // Only assign new objectId if we are creating new object - if (!this.data.objectId) { - this.data.objectId = cryptoUtils.newObjectId( - this.config.objectIdSize - ); - } - if (schema) { - Object.keys(schema.fields).forEach(fieldName => { - setRequiredFieldIfNeeded(fieldName, true); + // Only assign new objectId if we are creating new object + if (!this.data.objectId) { + this.data.objectId = cryptoUtils.newObjectId( + this.config.objectIdSize + ); + } + if (schema) { + Object.keys(schema.fields).forEach(fieldName => { + setRequiredFieldIfNeeded(fieldName, true); + }); + } + } else if (schema) { + Object.keys(this.data).forEach(fieldName => { + setRequiredFieldIfNeeded(fieldName, false); }); } - } else if (schema) { - Object.keys(this.data).forEach(fieldName => { - setRequiredFieldIfNeeded(fieldName, false); - }); - } - }); + }); } return Promise.resolve(); }; From fa0dbbb1d50d1281a0f05146c17fff401da67c3a Mon Sep 17 00:00:00 2001 From: = Date: Mon, 22 Jul 2019 01:54:09 -0700 Subject: [PATCH 07/12] Fix tests --- src/RestWrite.js | 99 ++++++++++++++++++++++++------------------------ 1 file changed, 50 insertions(+), 49 deletions(-) diff --git a/src/RestWrite.js b/src/RestWrite.js index cc642d044e..7e07f2f095 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -323,63 +323,64 @@ RestWrite.prototype.runBeforeLoginTrigger = async function(userData) { RestWrite.prototype.setRequiredFieldsIfNeeded = function() { if (this.data) { - return this.validSchemaController - .getOneSchema(this.className) - .then(schema => { - const setRequiredFieldIfNeeded = (fieldName, setDefault) => { + return this.validSchemaController.getAllClasses().then(allClasses => { + const schema = allClasses.find( + oneClass => oneClass.className === this.className + ); + const setRequiredFieldIfNeeded = (fieldName, setDefault) => { + if ( + this.data[fieldName] === undefined || + this.data[fieldName] === null || + this.data[fieldName] === '' || + (typeof this.data[fieldName] === 'object' && + this.data[fieldName].__op === 'Delete') + ) { if ( - this.data[fieldName] === undefined || - this.data[fieldName] === null || - this.data[fieldName] === '' || - (typeof this.data[fieldName] === 'object' && - this.data[fieldName].__op === 'Delete') + setDefault && + schema.fields[fieldName] && + schema.fields[fieldName].defaultValue && + this.data[fieldName] === undefined ) { - if ( - setDefault && - schema.fields[fieldName] && - schema.fields[fieldName].defaultValue && - this.data[fieldName] === undefined - ) { - this.data[fieldName] = schema.fields[fieldName].defaultValue; - this.storage.fieldsChangedByTrigger = - this.storage.fieldsChangedByTrigger || []; - if (this.storage.fieldsChangedByTrigger.indexOf(fieldName) < 0) { - this.storage.fieldsChangedByTrigger.push(fieldName); - } - } else if ( - schema.fields[fieldName] && - schema.fields[fieldName].required === true - ) { - throw new Parse.Error( - Parse.Error.VALIDATION_ERROR, - `${fieldName} is required` - ); + this.data[fieldName] = schema.fields[fieldName].defaultValue; + this.storage.fieldsChangedByTrigger = + this.storage.fieldsChangedByTrigger || []; + if (this.storage.fieldsChangedByTrigger.indexOf(fieldName) < 0) { + this.storage.fieldsChangedByTrigger.push(fieldName); } + } else if ( + schema.fields[fieldName] && + schema.fields[fieldName].required === true + ) { + throw new Parse.Error( + Parse.Error.VALIDATION_ERROR, + `${fieldName} is required` + ); } - }; + } + }; - // Add default fields - this.data.updatedAt = this.updatedAt; - if (!this.query) { - this.data.createdAt = this.updatedAt; + // Add default fields + this.data.updatedAt = this.updatedAt; + if (!this.query) { + this.data.createdAt = this.updatedAt; - // Only assign new objectId if we are creating new object - if (!this.data.objectId) { - this.data.objectId = cryptoUtils.newObjectId( - this.config.objectIdSize - ); - } - if (schema) { - Object.keys(schema.fields).forEach(fieldName => { - setRequiredFieldIfNeeded(fieldName, true); - }); - } - } else if (schema) { - Object.keys(this.data).forEach(fieldName => { - setRequiredFieldIfNeeded(fieldName, false); + // Only assign new objectId if we are creating new object + if (!this.data.objectId) { + this.data.objectId = cryptoUtils.newObjectId( + this.config.objectIdSize + ); + } + if (schema) { + Object.keys(schema.fields).forEach(fieldName => { + setRequiredFieldIfNeeded(fieldName, true); }); } - }); + } else if (schema) { + Object.keys(this.data).forEach(fieldName => { + setRequiredFieldIfNeeded(fieldName, false); + }); + } + }); } return Promise.resolve(); }; From c0f373d488b874ff5c3b531341cf7cc4a1d8e4c2 Mon Sep 17 00:00:00 2001 From: = Date: Wed, 24 Jul 2019 18:22:20 -0700 Subject: [PATCH 08/12] Test for creating a new class with field options --- spec/schemas.spec.js | 62 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 1cf5f7e1d9..5e7ccd6a1d 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -406,6 +406,68 @@ describe('schemas', () => { }); }); + it('responds with all fields and options when you create a class with field options', done => { + request({ + url: 'http://localhost:8378/1/schemas', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClassWithOptions', + fields: { + foo1: { type: 'Number' }, + foo2: { type: 'Number', required: true, defaultValue: 10 }, + foo3: { + type: 'String', + required: false, + defaultValue: 'some string', + }, + foo4: { type: 'Date', required: true }, + foo5: { type: 'Number', defaultValue: 5 }, + ptr: { type: 'Pointer', targetClass: 'SomeClass', required: false }, + }, + }, + }).then(async response => { + expect(response.data).toEqual({ + className: 'NewClassWithOptions', + fields: { + ACL: { type: 'ACL' }, + createdAt: { type: 'Date' }, + updatedAt: { type: 'Date' }, + objectId: { type: 'String' }, + foo1: { type: 'Number' }, + foo2: { type: 'Number', required: true, defaultValue: 10 }, + foo3: { + type: 'String', + required: false, + defaultValue: 'some string', + }, + foo4: { type: 'Date', required: true }, + foo5: { type: 'Number', defaultValue: 5 }, + ptr: { type: 'Pointer', targetClass: 'SomeClass', required: false }, + }, + classLevelPermissions: defaultClassLevelPermissions, + }); + const obj = new Parse.Object('NewClassWithOptions'); + try { + await obj.save(); + fail('should fail'); + } catch (e) { + expect(e.code).toEqual(142); + } + const date = new Date(); + obj.set('foo4', date); + await obj.save(); + expect(obj.get('foo1')).toBeUndefined(); + expect(obj.get('foo2')).toEqual(10); + expect(obj.get('foo3')).toEqual('some string'); + expect(obj.get('foo4')).toEqual(date); + expect(obj.get('foo5')).toEqual(5); + expect(obj.get('ptr')).toBeUndefined(); + done(); + }); + }); + it('responds with all fields when getting incomplete schema', done => { config.database .loadSchema() From 39780a3da7f283637537039c95161204898bcb6b Mon Sep 17 00:00:00 2001 From: = Date: Thu, 25 Jul 2019 15:37:08 -0700 Subject: [PATCH 09/12] Validate default value type --- spec/Schema.spec.js | 26 +++++++------- spec/schemas.spec.js | 56 +++++++++++++++++++++++++++++ src/Controllers/SchemaController.js | 39 +++++++++++++++++++- src/Controllers/types.js | 2 ++ 4 files changed, 109 insertions(+), 14 deletions(-) diff --git a/spec/Schema.spec.js b/spec/Schema.spec.js index aaede67e17..cf73927274 100644 --- a/spec/Schema.spec.js +++ b/spec/Schema.spec.js @@ -504,7 +504,7 @@ describe('SchemaController', () => { schema .addClassIfNotExists('_InvalidName', { foo: { type: 'String' } }) .catch(error => { - expect(error.error).toEqual( + expect(error.message).toEqual( 'Invalid classname: _InvalidName, classnames can only have alphanumeric characters and _, and must start with an alpha character ' ); done(); @@ -522,7 +522,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_KEY_NAME); - expect(error.error).toEqual('invalid field name: 0InvalidName'); + expect(error.message).toEqual('invalid field name: 0InvalidName'); done(); }); }); @@ -535,7 +535,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(136); - expect(error.error).toEqual('field objectId cannot be added'); + expect(error.message).toEqual('field objectId cannot be added'); done(); }); }); @@ -550,7 +550,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(136); - expect(error.error).toEqual('field localeIdentifier cannot be added'); + expect(error.message).toEqual('field localeIdentifier cannot be added'); done(); }); }); @@ -565,7 +565,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); - expect(error.error).toEqual('invalid JSON'); + expect(error.message).toEqual('invalid JSON'); done(); }); }); @@ -580,7 +580,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(135); - expect(error.error).toEqual('type Pointer needs a class name'); + expect(error.message).toEqual('type Pointer needs a class name'); done(); }); }); @@ -595,7 +595,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); - expect(error.error).toEqual('invalid JSON'); + expect(error.message).toEqual('invalid JSON'); done(); }); }); @@ -610,7 +610,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(135); - expect(error.error).toEqual('type Relation needs a class name'); + expect(error.message).toEqual('type Relation needs a class name'); done(); }); }); @@ -625,7 +625,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_JSON); - expect(error.error).toEqual('invalid JSON'); + expect(error.message).toEqual('invalid JSON'); done(); }); }); @@ -640,7 +640,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(error.error).toEqual( + expect(error.message).toEqual( 'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ' ); done(); @@ -657,7 +657,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INVALID_CLASS_NAME); - expect(error.error).toEqual( + expect(error.message).toEqual( 'Invalid classname: not a valid class name, classnames can only have alphanumeric characters and _, and must start with an alpha character ' ); done(); @@ -674,7 +674,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); - expect(error.error).toEqual('invalid field type: Unknown'); + expect(error.message).toEqual('invalid field type: Unknown'); done(); }); }); @@ -929,7 +929,7 @@ describe('SchemaController', () => { ) .catch(error => { expect(error.code).toEqual(Parse.Error.INCORRECT_TYPE); - expect(error.error).toEqual( + expect(error.message).toEqual( 'currently, only one GeoPoint field may exist in an object. Adding geo2 when geo1 already exists.' ); done(); diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index 5e7ccd6a1d..f56fc2725f 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -468,6 +468,62 @@ describe('schemas', () => { }); }); + it('validated the data type of default values when creating a new class', async () => { + try { + await request({ + url: 'http://localhost:8378/1/schemas', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClassWithValidation', + fields: { + foo: { type: 'String', defaultValue: 10 }, + }, + }, + }); + fail('should fail'); + } catch (e) { + expect(e.data.error).toEqual( + 'schema mismatch for NewClassWithValidation.foo default value; expected String but got Number' + ); + } + }); + + it('validated the data type of default values when adding new fields', async () => { + try { + await request({ + url: 'http://localhost:8378/1/schemas', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClassWithValidation', + fields: { + foo: { type: 'String', defaultValue: 'some value' }, + }, + }, + }); + await request({ + url: 'http://localhost:8378/1/schemas/NewClassWithValidation', + method: 'PUT', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClassWithValidation', + fields: { + foo2: { type: 'String', defaultValue: 10 }, + }, + }, + }); + fail('should fail'); + } catch (e) { + expect(e.data.error).toEqual( + 'schema mismatch for NewClassWithValidation.foo2 default value; expected String but got Number' + ); + } + }); + it('responds with all fields when getting incomplete schema', done => { config.database .loadSchema() diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index e3dfc040b9..00f62cfb41 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -654,6 +654,13 @@ export default class SchemaController { classLevelPermissions ); if (validationError) { + if (validationError instanceof Parse.Error) { + return Promise.reject(validationError); + } else if (validationError.code && validationError.error) { + return Promise.reject( + new Parse.Error(validationError.code, validationError.error) + ); + } return Promise.reject(validationError); } @@ -874,8 +881,23 @@ export default class SchemaController { error: 'field ' + fieldName + ' cannot be added', }; } - const error = fieldTypeIsInvalid(fields[fieldName]); + const type = fields[fieldName]; + const error = fieldTypeIsInvalid(type); if (error) return { code: error.code, error: error.message }; + if (type.defaultValue !== undefined) { + let defaultValueType = getType(type.defaultValue); + if (typeof defaultValueType === 'string') { + defaultValueType = { type: defaultValueType }; + } + if (!dbTypeMatchesObjectType(type, defaultValueType)) { + return { + code: Parse.Error.INCORRECT_TYPE, + error: `schema mismatch for ${className}.${fieldName} default value; expected ${typeToString( + type + )} but got ${typeToString(defaultValueType)}`, + }; + } + } } } @@ -940,6 +962,21 @@ export default class SchemaController { type = { type }; } + if (type.defaultValue !== undefined) { + let defaultValueType = getType(type.defaultValue); + if (typeof defaultValueType === 'string') { + defaultValueType = { type: defaultValueType }; + } + if (!dbTypeMatchesObjectType(type, defaultValueType)) { + throw new Parse.Error( + Parse.Error.INCORRECT_TYPE, + `schema mismatch for ${className}.${fieldName} default value; expected ${typeToString( + type + )} but got ${typeToString(defaultValueType)}` + ); + } + } + if (expectedType) { if (!dbTypeMatchesObjectType(expectedType, type)) { throw new Parse.Error( diff --git a/src/Controllers/types.js b/src/Controllers/types.js index 77a67f6c6d..2bd3298935 100644 --- a/src/Controllers/types.js +++ b/src/Controllers/types.js @@ -5,6 +5,8 @@ export type LoadSchemaOptions = { export type SchemaField = { type: string, targetClass?: ?string, + required?: ?boolean, + defaultValue?: ?any, }; export type SchemaFields = { [string]: SchemaField }; From 82006d9e8af1c762685f0c79888513b4185066d8 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 25 Jul 2019 15:52:38 -0700 Subject: [PATCH 10/12] fix lint (weird) --- src/Controllers/SchemaController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 00f62cfb41..88aeec3007 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -959,7 +959,7 @@ export default class SchemaController { const expectedType = this.getExpectedType(className, fieldName); if (typeof type === 'string') { - type = { type }; + type = { type, defaultValue: undefined }; } if (type.defaultValue !== undefined) { From d8d6144264210cafbc253d13e5ebb56078c9e5a2 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 25 Jul 2019 16:06:09 -0700 Subject: [PATCH 11/12] Fix lint another way --- src/Controllers/SchemaController.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index 88aeec3007..d6a95a5034 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -959,7 +959,7 @@ export default class SchemaController { const expectedType = this.getExpectedType(className, fieldName); if (typeof type === 'string') { - type = { type, defaultValue: undefined }; + type = ({ type }: SchemaField); } if (type.defaultValue !== undefined) { From dd1d9a5bd9ab65ef95d558f088c5d295dcee2bc0 Mon Sep 17 00:00:00 2001 From: = Date: Thu, 25 Jul 2019 16:35:14 -0700 Subject: [PATCH 12/12] Add tests for beforeSave trigger and solve small issue regarding the use of unset in the beforeSave trigger --- spec/schemas.spec.js | 105 +++++++++++++++++++++++++++++++++++++++++++ src/RestWrite.js | 4 +- 2 files changed, 108 insertions(+), 1 deletion(-) diff --git a/spec/schemas.spec.js b/spec/schemas.spec.js index f56fc2725f..e0969aad37 100644 --- a/spec/schemas.spec.js +++ b/spec/schemas.spec.js @@ -1066,6 +1066,111 @@ describe('schemas', () => { }); }); + it('should validate required fields and set default values after before save trigger', async () => { + await request({ + url: 'http://localhost:8378/1/schemas', + method: 'POST', + headers: masterKeyHeaders, + json: true, + body: { + className: 'NewClassForBeforeSaveTest', + fields: { + foo1: { type: 'String' }, + foo2: { type: 'String', required: true }, + foo3: { + type: 'String', + required: true, + defaultValue: 'some default value 3', + }, + foo4: { type: 'String', defaultValue: 'some default value 4' }, + }, + }, + }); + + Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => { + req.object.set('foo1', 'some value 1'); + req.object.set('foo2', 'some value 2'); + req.object.set('foo3', 'some value 3'); + req.object.set('foo4', 'some value 4'); + }); + + let obj = new Parse.Object('NewClassForBeforeSaveTest'); + await obj.save(); + + expect(obj.get('foo1')).toEqual('some value 1'); + expect(obj.get('foo2')).toEqual('some value 2'); + expect(obj.get('foo3')).toEqual('some value 3'); + expect(obj.get('foo4')).toEqual('some value 4'); + + Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => { + req.object.set('foo1', 'some value 1'); + req.object.set('foo2', 'some value 2'); + }); + + obj = new Parse.Object('NewClassForBeforeSaveTest'); + await obj.save(); + + expect(obj.get('foo1')).toEqual('some value 1'); + expect(obj.get('foo2')).toEqual('some value 2'); + expect(obj.get('foo3')).toEqual('some default value 3'); + expect(obj.get('foo4')).toEqual('some default value 4'); + + Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => { + req.object.set('foo1', 'some value 1'); + req.object.set('foo2', 'some value 2'); + req.object.set('foo3', undefined); + req.object.unset('foo4'); + }); + + obj = new Parse.Object('NewClassForBeforeSaveTest'); + obj.set('foo3', 'some value 3'); + obj.set('foo4', 'some value 4'); + await obj.save(); + + expect(obj.get('foo1')).toEqual('some value 1'); + expect(obj.get('foo2')).toEqual('some value 2'); + expect(obj.get('foo3')).toEqual('some default value 3'); + expect(obj.get('foo4')).toEqual('some default value 4'); + + Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => { + req.object.set('foo1', 'some value 1'); + req.object.set('foo2', undefined); + req.object.set('foo3', undefined); + req.object.unset('foo4'); + }); + + obj = new Parse.Object('NewClassForBeforeSaveTest'); + obj.set('foo2', 'some value 2'); + obj.set('foo3', 'some value 3'); + obj.set('foo4', 'some value 4'); + + try { + await obj.save(); + fail('should fail'); + } catch (e) { + expect(e.message).toEqual('foo2 is required'); + } + + Parse.Cloud.beforeSave('NewClassForBeforeSaveTest', req => { + req.object.set('foo1', 'some value 1'); + req.object.unset('foo2'); + req.object.set('foo3', undefined); + req.object.unset('foo4'); + }); + + obj = new Parse.Object('NewClassForBeforeSaveTest'); + obj.set('foo2', 'some value 2'); + obj.set('foo3', 'some value 3'); + obj.set('foo4', 'some value 4'); + + try { + await obj.save(); + fail('should fail'); + } catch (e) { + expect(e.message).toEqual('foo2 is required'); + } + }); + it('lets you add fields to system schema', done => { request({ method: 'POST', diff --git a/src/RestWrite.js b/src/RestWrite.js index 7e07f2f095..3ac917cd3a 100644 --- a/src/RestWrite.js +++ b/src/RestWrite.js @@ -339,7 +339,9 @@ RestWrite.prototype.setRequiredFieldsIfNeeded = function() { setDefault && schema.fields[fieldName] && schema.fields[fieldName].defaultValue && - this.data[fieldName] === undefined + (this.data[fieldName] === undefined || + (typeof this.data[fieldName] === 'object' && + this.data[fieldName].__op === 'Delete')) ) { this.data[fieldName] = schema.fields[fieldName].defaultValue; this.storage.fieldsChangedByTrigger =