From 132026d200ed1152a3f74a069f80f334a0413844 Mon Sep 17 00:00:00 2001 From: Drew Gross Date: Mon, 25 Apr 2016 11:51:27 -0700 Subject: [PATCH] Fix #1591 --- spec/MongoTransform.spec.js | 42 +++++---------- spec/Parse.Push.spec.js | 54 ------------------- .../Storage/Mongo/MongoStorageAdapter.js | 14 +++-- src/Adapters/Storage/Mongo/MongoTransform.js | 51 ++++++++---------- src/Controllers/DatabaseController.js | 21 +++++--- src/Controllers/SchemaController.js | 23 ++++++-- 6 files changed, 71 insertions(+), 134 deletions(-) diff --git a/spec/MongoTransform.spec.js b/spec/MongoTransform.spec.js index 59fee087b6..e309394691 100644 --- a/spec/MongoTransform.spec.js +++ b/spec/MongoTransform.spec.js @@ -23,13 +23,11 @@ var dummySchema = { }; -describe('parseObjectToMongoObjectForCreate', () => { +describe('parseObjectToMongoObject', () => { it('a basic number', (done) => { var input = {five: 5}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { - fields: {five: {type: 'Number'}} - }); + var output = transform.parseObjectToMongoObject(dummySchema, null, input); jequal(input, output); done(); }); @@ -39,7 +37,7 @@ describe('parseObjectToMongoObjectForCreate', () => { createdAt: "2015-10-06T21:24:50.332Z", updatedAt: "2015-10-06T21:24:50.332Z" }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObject(dummySchema, null, input); expect(output._created_at instanceof Date).toBe(true); expect(output._updated_at instanceof Date).toBe(true); done(); @@ -51,25 +49,21 @@ describe('parseObjectToMongoObjectForCreate', () => { objectId: 'myId', className: 'Blah', }; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {pointers: [pointer]},{ - fields: {pointers: {type: 'Array'}} - }); + var out = transform.parseObjectToMongoObject(dummySchema, null, {pointers: [pointer]}); jequal([pointer], out.pointers); done(); }); - //TODO: object creation requests shouldn't be seeing __op delete, it makes no sense to - //have __op delete in a new object. Figure out what this should actually be testing. - notWorking('a delete op', (done) => { + it('a delete op', (done) => { var input = {deleteMe: {__op: 'Delete'}}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObject(dummySchema, null, input); jequal(output, {}); done(); }); it('basic ACL', (done) => { var input = {ACL: {'0123': {'read': true, 'write': true}}}; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObject(dummySchema, null, input); // This just checks that it doesn't crash, but it should check format. done(); }); @@ -77,27 +71,21 @@ describe('parseObjectToMongoObjectForCreate', () => { describe('GeoPoints', () => { it('plain', (done) => { var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {location: geoPoint},{ - fields: {location: {type: 'GeoPoint'}} - }); + var out = transform.parseObjectToMongoObject(dummySchema, null, {location: geoPoint}); expect(out.location).toEqual([180, -180]); done(); }); it('in array', (done) => { var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, {locations: [geoPoint, geoPoint]},{ - fields: {locations: {type: 'Array'}} - }); + var out = transform.parseObjectToMongoObject(dummySchema, null, {locations: [geoPoint, geoPoint]}); expect(out.locations).toEqual([geoPoint, geoPoint]); done(); }); it('in sub-object', (done) => { var geoPoint = {__type: 'GeoPoint', longitude: 180, latitude: -180}; - var out = transform.parseObjectToMongoObjectForCreate(dummySchema, null, { locations: { start: geoPoint }},{ - fields: {locations: {type: 'Object'}} - }); + var out = transform.parseObjectToMongoObject(dummySchema, null, { locations: { start: geoPoint }}); expect(out).toEqual({ locations: { start: geoPoint } }); done(); }); @@ -208,9 +196,7 @@ describe('transform schema key changes', () => { var input = { somePointer: {__type: 'Pointer', className: 'Micro', objectId: 'oft'} }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { - fields: {somePointer: {type: 'Pointer'}} - }); + var output = transform.parseObjectToMongoObject(dummySchema, null, input); expect(typeof output._p_somePointer).toEqual('string'); expect(output._p_somePointer).toEqual('Micro$oft'); done(); @@ -220,9 +206,7 @@ describe('transform schema key changes', () => { var input = { userPointer: {__type: 'Pointer', className: '_User', objectId: 'qwerty'} }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input, { - fields: {userPointer: {type: 'Pointer'}} - }); + var output = transform.parseObjectToMongoObject(dummySchema, null, input); expect(typeof output._p_userPointer).toEqual('string'); expect(output._p_userPointer).toEqual('_User$qwerty'); done(); @@ -235,7 +219,7 @@ describe('transform schema key changes', () => { "Kevin": { "write": true } } }; - var output = transform.parseObjectToMongoObjectForCreate(dummySchema, null, input); + var output = transform.parseObjectToMongoObject(dummySchema, null, input); expect(typeof output._rperm).toEqual('object'); expect(typeof output._wperm).toEqual('object'); expect(output.ACL).toBeUndefined(); diff --git a/spec/Parse.Push.spec.js b/spec/Parse.Push.spec.js index c9b6e8ec08..5507b0f300 100644 --- a/spec/Parse.Push.spec.js +++ b/spec/Parse.Push.spec.js @@ -1,6 +1,5 @@ 'use strict'; -let request = require('request'); describe('Parse.Push', () => { @@ -90,57 +89,4 @@ describe('Parse.Push', () => { done(); }); }); - - it('should not allow clients to query _PushStatus', done => { - setup() - .then(() => Parse.Push.send({ - where: { - deviceType: 'ios' - }, - data: { - badge: 'increment', - alert: 'Hello world!' - } - }, {useMasterKey: true})) - .then(() => { - request.get({ - url: 'http://localhost:8378/1/classes/_PushStatus', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - }, - }, (error, response, body) => { - expect(body.results.length).toEqual(0); - done(); - }); - }); - }); - - it('should allow master key to query _PushStatus', done => { - setup() - .then(() => Parse.Push.send({ - where: { - deviceType: 'ios' - }, - data: { - badge: 'increment', - alert: 'Hello world!' - } - }, {useMasterKey: true})) - .then(() => { - request.get({ - url: 'http://localhost:8378/1/classes/_PushStatus', - json: true, - headers: { - 'X-Parse-Application-Id': 'test', - 'X-Parse-Master-Key': 'test', - }, - }, (error, response, body) => { - expect(body.results.length).toEqual(1); - expect(body.results[0].query).toEqual('{"deviceType":"ios"}'); - expect(body.results[0].payload).toEqual('{"badge":"increment","alert":"Hello world!"}'); - done(); - }); - }); - }); }); diff --git a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js index c83200be52..51c0e38e04 100644 --- a/src/Adapters/Storage/Mongo/MongoStorageAdapter.js +++ b/src/Adapters/Storage/Mongo/MongoStorageAdapter.js @@ -145,16 +145,14 @@ export class MongoStorageAdapter { // this adapter doesn't know about the schema, return a promise that rejects with // undefined as the reason. getOneSchema(className) { - return this.schemaCollection() - .then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className)); + return this.schemaCollection().then(schemasCollection => schemasCollection._fechOneSchemaFrom_SCHEMA(className)); } - // TODO: As yet not particularly well specified. Creates an object. Shouldn't need the - // schemaController, but MongoTransform still needs it :( maybe shouldn't even need the schema, - // 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, object, schemaController, parseFormatSchema) { - const mongoObject = transform.parseObjectToMongoObjectForCreate(schemaController, className, object, parseFormatSchema); + // TODO: As yet not particularly well specified. Creates an object. Does it really need the schema? + // or can it fetch the schema itself? Also the schema is not currently a Parse format schema, and it + // should be, if we are passing it at all. + createObject(className, object, schema) { + const mongoObject = transform.parseObjectToMongoObject(schema, className, object); return this.adaptiveCollection(className) .then(collection => collection.insertOne(mongoObject)); } diff --git a/src/Adapters/Storage/Mongo/MongoTransform.js b/src/Adapters/Storage/Mongo/MongoTransform.js index 34f0f15779..971a5f6fe2 100644 --- a/src/Adapters/Storage/Mongo/MongoTransform.js +++ b/src/Adapters/Storage/Mongo/MongoTransform.js @@ -21,13 +21,9 @@ var Parse = require('parse/node').Parse; // validate: true indicates that key names are to be validated. // // Returns an object with {key: key, value: value}. -export function transformKeyValue(schema, className, restKey, restValue, { - inArray, - inObject, - query, - update, - validate, -} = {}) { +export function transformKeyValue(schema, className, restKey, restValue, options) { + options = options || {}; + // Check if the schema is known since it's a built-in field. var key = restKey; var timeField = false; @@ -66,7 +62,7 @@ export function transformKeyValue(schema, className, restKey, restValue, { return {key: key, value: restValue}; break; case '$or': - if (!query) { + if (!options.query) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $or in queries'); } @@ -79,7 +75,7 @@ export function transformKeyValue(schema, className, restKey, restValue, { }); return {key: '$or', value: mongoSubqueries}; case '$and': - if (!query) { + if (!options.query) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'you can only use $and in queries'); } @@ -95,7 +91,7 @@ export function transformKeyValue(schema, className, restKey, restValue, { // Other auth data var authDataMatch = key.match(/^authData\.([a-zA-Z0-9_]+)\.id$/); if (authDataMatch) { - if (query) { + if (options.query) { var provider = authDataMatch[1]; // Special-case auth data. return {key: '_auth_data_'+provider+'.id', value: restValue}; @@ -104,7 +100,7 @@ export function transformKeyValue(schema, className, restKey, restValue, { 'can only query on ' + key); break; }; - if (validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { + if (options.validate && !key.match(/^[a-zA-Z][a-zA-Z0-9_\.]*$/)) { throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, 'invalid key name: ' + key); } @@ -121,24 +117,24 @@ export function transformKeyValue(schema, className, restKey, restValue, { (!expected && restValue && restValue.__type == 'Pointer')) { key = '_p_' + key; } - var expectedTypeIsArray = (expected && expected.type === 'Array'); + var inArray = (expected && expected.type === 'Array'); // Handle query constraints - if (query) { - value = transformConstraint(restValue, expectedTypeIsArray); + if (options.query) { + value = transformConstraint(restValue, inArray); if (value !== CannotTransform) { return {key: key, value: value}; } } - if (expectedTypeIsArray && query && !(restValue instanceof Array)) { + if (inArray && options.query && !(restValue instanceof Array)) { return { key: key, value: { '$all' : [restValue] } }; } // Handle atomic values - var value = transformAtom(restValue, false, { inArray, inObject }); + var value = transformAtom(restValue, false, options); if (value !== CannotTransform) { if (timeField && (typeof value === 'string')) { value = new Date(value); @@ -154,7 +150,7 @@ export function transformKeyValue(schema, className, restKey, restValue, { // Handle arrays if (restValue instanceof Array) { - if (query) { + if (options.query) { throw new Parse.Error(Parse.Error.INVALID_JSON, 'cannot use array as query param'); } @@ -166,7 +162,7 @@ export function transformKeyValue(schema, className, restKey, restValue, { } // Handle update operators - value = transformUpdateOperator(restValue, !update); + value = transformUpdateOperator(restValue, !options.update); if (value !== CannotTransform) { return {key: key, value: value}; } @@ -290,21 +286,16 @@ const parseObjectKeyValueToMongoObjectKeyValue = ( // Main exposed method to create new objects. // restCreate is the "create" clause in REST API form. -function parseObjectToMongoObjectForCreate(schema, className, restCreate, parseFormatSchema) { +// Returns the mongo form of the object. +function parseObjectToMongoObject(schema, className, restCreate) { if (className == '_User') { restCreate = transformAuthData(restCreate); } var mongoCreate = transformACL(restCreate); - for (let restKey in restCreate) { - let { key, value } = parseObjectKeyValueToMongoObjectKeyValue( - schema, - className, - restKey, - restCreate[restKey], - parseFormatSchema - ); - if (value !== undefined) { - mongoCreate[key] = value; + for (var restKey in restCreate) { + var out = transformKeyValue(schema, className, restKey, restCreate[restKey]); + if (out.value !== undefined) { + mongoCreate[out.key] = out.value; } } return mongoCreate; @@ -1006,7 +997,7 @@ var FileCoder = { module.exports = { transformKey, - parseObjectToMongoObjectForCreate, + parseObjectToMongoObject, transformUpdate, transformWhere, transformSelect, diff --git a/src/Controllers/DatabaseController.js b/src/Controllers/DatabaseController.js index 926b1b1717..f3f1e993c0 100644 --- a/src/Controllers/DatabaseController.js +++ b/src/Controllers/DatabaseController.js @@ -336,19 +336,24 @@ DatabaseController.prototype.create = function(className, object, { acl } = {}) let originalObject = object; object = deepcopy(object); + var schema; var isMaster = acl === undefined; var aclGroup = acl || []; return this.validateClassName(className) - .then(() => this.loadSchema()) - .then(schemaController => { - return (isMaster ? Promise.resolve() : schemaController.validatePermission(className, aclGroup, 'create')) + .then(() => this.loadSchema()) + .then(s => { + schema = s; + if (!isMaster) { + return schema.validatePermission(className, aclGroup, 'create'); + } + return Promise.resolve(); + }) .then(() => this.handleRelationUpdates(className, null, object)) - .then(() => schemaController.enforceClassExists(className)) - .then(() => schemaController.getOneSchema(className)) - .then(schema => this.adapter.createObject(className, object, schemaController, schema)) - .then(result => sanitizeDatabaseResult(originalObject, result.ops[0])); - }) + .then(() => this.adapter.createObject(className, object, schema)) + .then(result => { + return sanitizeDatabaseResult(originalObject, result.ops[0]); + }); }; DatabaseController.prototype.canAddField = function(schema, className, object, aclGroup) { diff --git a/src/Controllers/SchemaController.js b/src/Controllers/SchemaController.js index cf6b898a3e..0bc3166a42 100644 --- a/src/Controllers/SchemaController.js +++ b/src/Controllers/SchemaController.js @@ -91,7 +91,7 @@ const requiredColumns = Object.freeze({ _Role: ["name", "ACL"] }); -const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus']); +const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product']); // 10 alpha numberic chars + uppercase const userIdRegex = /^[a-zA-Z0-9]{10}$/; @@ -355,8 +355,12 @@ 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) { + // If 'freeze' is true, refuse to update the schema. + // WARNING: this function has side-effects, and doesn't actually + // do any validation of the format of the className. You probably + // should use classNameIsValid or addClassIfNotExists or something + // like that instead. TODO: rename or remove this function. + validateClassName(className, freeze) { if (this.data[className]) { return Promise.resolve(this); } @@ -376,7 +380,7 @@ class SchemaController { return this.reloadData(); }).then(() => { // Ensure that the schema now validates - return this.enforceClassExists(className, true); + return this.validateClassName(className, true); }, () => { // The schema still doesn't validate. Give up throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate'); @@ -557,7 +561,7 @@ class SchemaController { // valid. validateObject(className, object, query) { let geocount = 0; - let promise = this.enforceClassExists(className); + let promise = this.validateClassName(className); for (let fieldName in object) { if (object[fieldName] === undefined) { continue; @@ -668,6 +672,15 @@ class SchemaController { return this.reloadData().then(() => !!(this.data[className])); } + // Helper function to check if a field is a pointer, returns true or false. + isPointer(className, key) { + let expected = this.getExpectedType(className, key); + if (expected && expected.charAt(0) == '*') { + return true; + } + return false; + }; + getRelationFields(className) { if (this.data && this.data[className]) { let classData = this.data[className];