Move mongo field type logic into mongoadapter (#1432)

This commit is contained in:
Drew
2016-04-12 05:06:00 -07:00
committed by Florent Vilmart
parent e9e561f5e8
commit 99ecbb39f5
2 changed files with 145 additions and 140 deletions

View File

@@ -76,6 +76,23 @@ function _mongoSchemaObjectFromNameFields(name: string, fields) {
return object; return object;
} }
// Returns a type suitable for inserting into mongo _SCHEMA collection.
// Does no validation. That is expected to be done in Parse Server.
function parseFieldTypeToMongoFieldType({ type, targetClass }) {
switch (type) {
case 'Pointer': return `*${targetClass}`;
case 'Relation': return `relation<${targetClass}>`;
case 'Number': return 'number';
case 'String': return 'string';
case 'Boolean': return 'boolean';
case 'Date': return 'date';
case 'Object': return 'object';
case 'Array': return 'array';
case 'GeoPoint': return 'geopoint';
case 'File': return 'file';
}
}
class MongoSchemaCollection { class MongoSchemaCollection {
_collection: MongoCollection; _collection: MongoCollection;
@@ -148,4 +165,8 @@ MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
// into the database adapter yet. We will remove this before too long. // into the database adapter yet. We will remove this before too long.
MongoSchemaCollection._DONOTUSEmongoFieldToParseSchemaField = mongoFieldToParseSchemaField MongoSchemaCollection._DONOTUSEmongoFieldToParseSchemaField = mongoFieldToParseSchemaField
// Exported because we haven't moved all mongo schema format related logic
// into the database adapter yet. We will remove this before too long.
MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType = parseFieldTypeToMongoFieldType;
export default MongoSchemaCollection export default MongoSchemaCollection

View File

@@ -14,8 +14,8 @@
// different databases. // different databases.
// TODO: hide all schema logic inside the database adapter. // TODO: hide all schema logic inside the database adapter.
var Parse = require('parse/node').Parse; const Parse = require('parse/node').Parse;
var transform = require('./transform'); const transform = require('./transform');
import MongoSchemaCollection from './Adapters/Storage/Mongo/MongoSchemaCollection'; import MongoSchemaCollection from './Adapters/Storage/Mongo/MongoSchemaCollection';
import _ from 'lodash'; import _ from 'lodash';
@@ -132,8 +132,8 @@ function validateCLP(perms) {
}); });
}); });
} }
var joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/; const joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
var classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/; const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
function classNameIsValid(className) { function classNameIsValid(className) {
// Valid classes must: // Valid classes must:
return ( return (
@@ -169,47 +169,37 @@ function invalidClassNameMessage(className) {
return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character '; return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
} }
// Returns { error: "message", code: ### } if the type could not be const invalidJsonError = new Parse.Error(Parse.Error.INVALID_JSON, "invalid JSON");
// converted, otherwise returns a returns { result: "mongotype" } const validNonRelationOrPointerTypes = [
// where mongotype is suitable for inserting into mongo _SCHEMA collection 'Number',
function schemaAPITypeToMongoFieldType(type) { 'String',
var invalidJsonError = { error: "invalid JSON", code: Parse.Error.INVALID_JSON }; 'Boolean',
if (type.type == 'Pointer') { 'Date',
if (!type.targetClass) { 'Object',
return { error: 'type Pointer needs a class name', code: 135 }; 'Array',
} else if (typeof type.targetClass !== 'string') { 'GeoPoint',
return invalidJsonError; 'File',
} else if (!classNameIsValid(type.targetClass)) { ];
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME }; // Returns an error suitable for throwing if the type is invalid
} else { const fieldTypeIsInvalid = ({ type, targetClass }) => {
return { result: '*' + type.targetClass }; if (['Pointer', 'Relation'].includes(type)) {
} if (!targetClass) {
} return new Parse.Error(135, `type ${type} needs a class name`);
if (type.type == 'Relation') { } else if (typeof targetClass !== 'string') {
if (!type.targetClass) { return invalidJsonError;
return { error: 'type Relation needs a class name', code: 135 }; } else if (!classNameIsValid(targetClass)) {
} else if (typeof type.targetClass !== 'string') { return new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(targetClass));
return invalidJsonError; } else {
} else if (!classNameIsValid(type.targetClass)) { return undefined;
return { error: invalidClassNameMessage(type.targetClass), code: Parse.Error.INVALID_CLASS_NAME }; }
} else { }
return { result: 'relation<' + type.targetClass + '>' }; if (typeof type !== 'string') {
} return invalidJsonError;
} }
if (typeof type.type !== 'string') { if (!validNonRelationOrPointerTypes.includes(type)) {
return { error: "invalid JSON", code: Parse.Error.INVALID_JSON }; return new Parse.Error(Parse.Error.INCORRECT_TYPE, `invalid field type: ${type}`);
} }
switch (type.type) { return undefined;
default: return { error: 'invalid field type: ' + type.type, code: Parse.Error.INCORRECT_TYPE };
case 'Number': return { result: 'number' };
case 'String': return { result: 'string' };
case 'Boolean': return { result: 'boolean' };
case 'Date': return { result: 'date' };
case 'Object': return { result: 'object' };
case 'Array': return { result: 'array' };
case 'GeoPoint': return { result: 'geopoint' };
case 'File': return { result: 'file' };
}
} }
// Stores the entire schema of the app in a weird hybrid format somewhere between // Stores the entire schema of the app in a weird hybrid format somewhere between
@@ -244,9 +234,8 @@ class Schema {
// createdAt and updatedAt are wacky and have legacy baggage // createdAt and updatedAt are wacky and have legacy baggage
parseFormatSchema.createdAt = { type: 'String' }; parseFormatSchema.createdAt = { type: 'String' };
parseFormatSchema.updatedAt = { type: 'String' }; parseFormatSchema.updatedAt = { type: 'String' };
this.data[schema.className] = _.mapValues(parseFormatSchema, parseField => //Necessary because we still use the mongo type internally here :(
schemaAPITypeToMongoFieldType(parseField).result this.data[schema.className] = _.mapValues(parseFormatSchema, MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType);
);
this.perms[schema.className] = schema.classLevelPermissions; this.perms[schema.className] = schema.classLevelPermissions;
}); });
@@ -332,7 +321,7 @@ class Schema {
// Returns whether the schema knows the type of all these keys. // Returns whether the schema knows the type of all these keys.
hasKeys(className, keys) { hasKeys(className, keys) {
for (var key of keys) { for (let key of keys) {
if (!this.data[className] || !this.data[className][key]) { if (!this.data[className] || !this.data[className][key]) {
return false; return false;
} }
@@ -380,7 +369,7 @@ class Schema {
return Promise.resolve(); return Promise.resolve();
} }
validateCLP(perms); validateCLP(perms);
var update = { let update = {
_metadata: { _metadata: {
class_permissions: perms class_permissions: perms
} }
@@ -397,72 +386,75 @@ class Schema {
// The className must already be validated. // The className must already be validated.
// If 'freeze' is true, refuse to update the schema for this field. // If 'freeze' is true, refuse to update the schema for this field.
validateField(className, fieldName, type, freeze) { validateField(className, fieldName, type, freeze) {
// Just to check that the fieldName is valid return this.reloadData().then(() => {
transform.transformKey(this, className, fieldName); // Just to check that the fieldName is valid
transform.transformKey(this, className, fieldName);
if( fieldName.indexOf(".") > 0 ) { if( fieldName.indexOf(".") > 0 ) {
// subdocument key (x.y) => ok if x is of type 'object' // subdocument key (x.y) => ok if x is of type 'object'
fieldName = fieldName.split(".")[ 0 ]; fieldName = fieldName.split(".")[ 0 ];
type = 'object'; type = 'object';
}
let expected = this.data[className][fieldName];
if (expected) {
expected = (expected === 'map' ? 'object' : expected);
if (expected === type) {
return Promise.resolve(this);
} else {
throw new Parse.Error(
Parse.Error.INCORRECT_TYPE,
`schema mismatch for ${className}.${fieldName}; expected ${expected} but got ${type}`
);
} }
}
if (freeze) { let expected = this.data[className][fieldName];
throw new Parse.Error(Parse.Error.INVALID_JSON, `schema is frozen, cannot add ${fieldName} field`); if (expected) {
} expected = (expected === 'map' ? 'object' : expected);
if (expected === type) {
// We don't have this field, but if the value is null or undefined, return Promise.resolve(this);
// we won't update the schema until we get a value with a type. } else {
if (!type) {
return Promise.resolve(this);
}
if (type === 'geopoint') {
// Make sure there are not other geopoint fields
for (var otherKey in this.data[className]) {
if (this.data[className][otherKey] === 'geopoint') {
throw new Parse.Error( throw new Parse.Error(
Parse.Error.INCORRECT_TYPE, Parse.Error.INCORRECT_TYPE,
'there can only be one geopoint field in a class'); `schema mismatch for ${className}.${fieldName}; expected ${expected} but got ${type}`
);
} }
} }
}
// We don't have this field. Update the schema. if (freeze) {
// Note that we use the $exists guard and $set to avoid race throw new Parse.Error(Parse.Error.INVALID_JSON, `schema is frozen, cannot add ${fieldName} field`);
// conditions in the database. This is important! }
let query = {};
query[fieldName] = { '$exists': false }; // We don't have this field, but if the value is null or undefined,
var update = {}; // we won't update the schema until we get a value with a type.
update[fieldName] = type; if (!type) {
update = {'$set': update}; return Promise.resolve(this);
return this._collection.upsertSchema(className, query, update).then(() => { }
// The update succeeded. Reload the schema
return this.reloadData(); if (type === 'geopoint') {
}, () => { // Make sure there are not other geopoint fields
// The update failed. This can be okay - it might have been a race for (let otherKey in this.data[className]) {
// condition where another client updated the schema in the same if (this.data[className][otherKey] === 'geopoint') {
// way that we wanted to. So, just reload the schema throw new Parse.Error(
return this.reloadData(); Parse.Error.INCORRECT_TYPE,
}).then(() => { 'there can only be one geopoint field in a class');
// 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, // We don't have this field. Update the schema.
'schema key will not revalidate'); // Note that we use the $exists guard and $set to avoid race
// conditions in the database. This is important!
let query = {};
query[fieldName] = { '$exists': false };
let update = {};
update[fieldName] = type;
update = {'$set': update};
return this._collection.upsertSchema(className, query, update).then(() => {
// The update succeeded. Reload the schema
return this.reloadData();
}, () => {
// The update failed. This can be okay - it might have been a race
// condition where another client updated the schema in the same
// way that we wanted to. So, just reload the schema
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
console.log(error)
throw new Parse.Error(Parse.Error.INVALID_JSON,
'schema key will not revalidate');
});
}); });
} }
@@ -526,13 +518,13 @@ class Schema {
// Returns a promise that resolves to the new schema if this object is // Returns a promise that resolves to the new schema if this object is
// valid. // valid.
validateObject(className, object, query) { validateObject(className, object, query) {
var geocount = 0; let geocount = 0;
var promise = this.validateClassName(className); let promise = this.validateClassName(className);
for (let fieldName in object) { for (let fieldName in object) {
if (object[fieldName] === undefined) { if (object[fieldName] === undefined) {
continue; continue;
} }
var expected = getType(object[fieldName]); let expected = getType(object[fieldName]);
if (expected === 'geopoint') { if (expected === 'geopoint') {
geocount++; geocount++;
} }
@@ -551,7 +543,6 @@ class Schema {
// Every object has ACL implicitly. // Every object has ACL implicitly.
continue; continue;
} }
promise = thenValidateField(promise, className, fieldName, expected); promise = thenValidateField(promise, className, fieldName, expected);
} }
promise = thenValidateRequiredColumns(promise, className, object, query); promise = thenValidateRequiredColumns(promise, className, object, query);
@@ -560,12 +551,12 @@ class Schema {
// Validates that all the properties are set for the object // Validates that all the properties are set for the object
validateRequiredColumns(className, object, query) { validateRequiredColumns(className, object, query) {
var columns = requiredColumns[className]; let columns = requiredColumns[className];
if (!columns || columns.length == 0) { if (!columns || columns.length == 0) {
return Promise.resolve(this); return Promise.resolve(this);
} }
var missingColumns = columns.filter(function(column){ let missingColumns = columns.filter(function(column){
if (query && query.objectId) { if (query && query.objectId) {
if (object[column] && typeof object[column] === "object") { if (object[column] && typeof object[column] === "object") {
// Trying to delete a required column // Trying to delete a required column
@@ -591,14 +582,14 @@ class Schema {
if (!this.perms[className] || !this.perms[className][operation]) { if (!this.perms[className] || !this.perms[className][operation]) {
return Promise.resolve(); return Promise.resolve();
} }
var perms = this.perms[className][operation]; let perms = this.perms[className][operation];
// Handle the public scenario quickly // Handle the public scenario quickly
if (perms['*']) { if (perms['*']) {
return Promise.resolve(); return Promise.resolve();
} }
// Check permissions against the aclGroup provided (array of userId/roles) // Check permissions against the aclGroup provided (array of userId/roles)
var found = false; let found = false;
for (var i = 0; i < aclGroup.length && !found; i++) { for (let i = 0; i < aclGroup.length && !found; i++) {
if (perms[aclGroup[i]]) { if (perms[aclGroup[i]]) {
found = true; found = true;
} }
@@ -628,7 +619,7 @@ class Schema {
// Helper function to check if a field is a pointer, returns true or false. // Helper function to check if a field is a pointer, returns true or false.
isPointer(className, key) { isPointer(className, key) {
var expected = this.getExpectedType(className, key); let expected = this.getExpectedType(className, key);
if (expected && expected.charAt(0) == '*') { if (expected && expected.charAt(0) == '*') {
return true; return true;
} }
@@ -661,7 +652,7 @@ function load(collection) {
} }
// Returns { code, error } if invalid, or { result }, an object // Returns { code, error } if invalid, or { result }, an object
// suitable for inserting into _SCHEMA collection, otherwise // suitable for inserting into _SCHEMA collection, otherwise.
function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) { function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) {
if (!classNameIsValid(className)) { if (!classNameIsValid(className)) {
return { return {
@@ -670,7 +661,7 @@ function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPe
}; };
} }
for (var fieldName in fields) { for (let fieldName in fields) {
if (!fieldNameIsValid(fieldName)) { if (!fieldNameIsValid(fieldName)) {
return { return {
code: Parse.Error.INVALID_KEY_NAME, code: Parse.Error.INVALID_KEY_NAME,
@@ -683,32 +674,26 @@ function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPe
error: 'field ' + fieldName + ' cannot be added', error: 'field ' + fieldName + ' cannot be added',
}; };
} }
const error = fieldTypeIsInvalid(fields[fieldName]);
if (error) return { code: error.code, error: error.message };
} }
var mongoObject = { let mongoObject = {
_id: className, _id: className,
objectId: 'string', objectId: 'string',
updatedAt: 'string', updatedAt: 'string',
createdAt: 'string' createdAt: 'string'
}; };
for (var fieldName in defaultColumns[className]) { for (let fieldName in defaultColumns[className]) {
var validatedField = schemaAPITypeToMongoFieldType(defaultColumns[className][fieldName]); mongoObject[fieldName] = MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType(defaultColumns[className][fieldName]);
if (!validatedField.result) {
return validatedField;
}
mongoObject[fieldName] = validatedField.result;
} }
for (var fieldName in fields) { for (let fieldName in fields) {
var validatedField = schemaAPITypeToMongoFieldType(fields[fieldName]); mongoObject[fieldName] = MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType(fields[fieldName]);
if (!validatedField.result) {
return validatedField;
}
mongoObject[fieldName] = validatedField.result;
} }
var geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint'); let geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
if (geoPoints.length > 1) { if (geoPoints.length > 1) {
return { return {
code: Parse.Error.INCORRECT_TYPE, code: Parse.Error.INCORRECT_TYPE,
@@ -735,20 +720,20 @@ function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPe
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it // to mongoSchemaFromFieldsAndClassName. No validation is done here, it
// is done in mongoSchemaFromFieldsAndClassName. // is done in mongoSchemaFromFieldsAndClassName.
function buildMergedSchemaObject(mongoObject, putRequest) { function buildMergedSchemaObject(mongoObject, putRequest) {
var newSchema = {}; let newSchema = {};
let sysSchemaField = Object.keys(defaultColumns).indexOf(mongoObject._id) === -1 ? [] : Object.keys(defaultColumns[mongoObject._id]); let sysSchemaField = Object.keys(defaultColumns).indexOf(mongoObject._id) === -1 ? [] : Object.keys(defaultColumns[mongoObject._id]);
for (var oldField in mongoObject) { for (let oldField in mongoObject) {
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') { if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) { if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) {
continue; continue;
} }
var fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete' let fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
if (!fieldIsDeleted) { if (!fieldIsDeleted) {
newSchema[oldField] = MongoSchemaCollection._DONOTUSEmongoFieldToParseSchemaField(mongoObject[oldField]); newSchema[oldField] = MongoSchemaCollection._DONOTUSEmongoFieldToParseSchemaField(mongoObject[oldField]);
} }
} }
} }
for (var newField in putRequest) { for (let newField in putRequest) {
if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') { if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) { if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) {
continue; continue;
@@ -781,7 +766,7 @@ function thenValidateRequiredColumns(schemaPromise, className, object, query) {
// The output should be a valid schema value. // The output should be a valid schema value.
// TODO: ensure that this is compatible with the format used in Open DB // TODO: ensure that this is compatible with the format used in Open DB
function getType(obj) { function getType(obj) {
var type = typeof obj; let type = typeof obj;
switch(type) { switch(type) {
case 'boolean': case 'boolean':
case 'string': case 'string':
@@ -863,7 +848,6 @@ export {
load, load,
classNameIsValid, classNameIsValid,
invalidClassNameMessage, invalidClassNameMessage,
schemaAPITypeToMongoFieldType,
buildMergedSchemaObject, buildMergedSchemaObject,
systemClasses, systemClasses,
defaultColumns, defaultColumns,