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;
}
// 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 {
_collection: MongoCollection;
@@ -148,4 +165,8 @@ MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
// into the database adapter yet. We will remove this before too long.
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

View File

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