Schema.js database agnostic (#1468)

* Schema.js database agnostic

* nits
This commit is contained in:
Florent Vilmart
2016-04-12 17:39:27 -04:00
parent c419106a38
commit c050a65d49
7 changed files with 177 additions and 163 deletions

View File

@@ -685,9 +685,10 @@ describe('Schema', () => {
.then(() => schema.reloadData()) .then(() => schema.reloadData())
.then(() => { .then(() => {
expect(schema['data']['NewClass']).toEqual({ expect(schema['data']['NewClass']).toEqual({
objectId: 'string', objectId: { type: 'String' },
updatedAt: 'string', updatedAt: { type: 'Date' },
createdAt: 'string' createdAt: { type: 'Date' },
ACL: { type: 'ACL' }
}); });
done(); done();
}); });
@@ -747,7 +748,7 @@ describe('Schema', () => {
it('can merge schemas', done => { it('can merge schemas', done => {
expect(Schema.buildMergedSchemaObject({ expect(Schema.buildMergedSchemaObject({
_id: 'SomeClass', _id: 'SomeClass',
someType: 'number' someType: { type: 'Number' }
}, { }, {
newType: {type: 'Number'} newType: {type: 'Number'}
})).toEqual({ })).toEqual({
@@ -760,8 +761,8 @@ describe('Schema', () => {
it('can merge deletions', done => { it('can merge deletions', done => {
expect(Schema.buildMergedSchemaObject({ expect(Schema.buildMergedSchemaObject({
_id: 'SomeClass', _id: 'SomeClass',
someType: 'number', someType: { type: 'Number' },
outDatedType: 'string', outDatedType: { type: 'String' },
},{ },{
newType: {type: 'GeoPoint'}, newType: {type: 'GeoPoint'},
outDatedType: {__op: 'Delete'}, outDatedType: {__op: 'Delete'},
@@ -775,16 +776,16 @@ describe('Schema', () => {
it('ignore default field when merge with system class', done => { it('ignore default field when merge with system class', done => {
expect(Schema.buildMergedSchemaObject({ expect(Schema.buildMergedSchemaObject({
_id: '_User', _id: '_User',
username: 'string', username: { type: 'String' },
password: 'string', password: { type: 'String' },
authData: 'object', authData: { type: 'Object' },
email: 'string', email: { type: 'String' },
emailVerified: 'boolean' emailVerified: { type: 'Boolean' },
},{ },{
authData: {type: 'string'}, authData: { type: 'String' },
customField: {type: 'string'}, customField: { type: 'String' },
})).toEqual({ })).toEqual({
customField: {type: 'string'} customField: { type: 'String' }
}); });
done(); done();
}); });

View File

@@ -8,11 +8,11 @@ var dummySchema = {
data: {}, data: {},
getExpectedType: function(className, key) { getExpectedType: function(className, key) {
if (key == 'userPointer') { if (key == 'userPointer') {
return '*_User'; return { type: 'Pointer', targetClass: '_User' };
} else if (key == 'picture') { } else if (key == 'picture') {
return 'file'; return { type: 'File' };
} else if (key == 'location') { } else if (key == 'location') {
return 'geopoint'; return { type: 'GeoPoint' };
} }
return; return;
}, },

View File

@@ -93,6 +93,33 @@ function parseFieldTypeToMongoFieldType({ type, targetClass }) {
} }
} }
// Returns { code, error } if invalid, or { result }, an object
// suitable for inserting into _SCHEMA collection, otherwise.
function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) {
let mongoObject = {
_id: className,
objectId: 'string',
updatedAt: 'string',
createdAt: 'string'
};
for (let fieldName in fields) {
mongoObject[fieldName] = parseFieldTypeToMongoFieldType(fields[fieldName]);
}
if (typeof classLevelPermissions !== 'undefined') {
mongoObject._metadata = mongoObject._metadata || {};
if (!classLevelPermissions) {
delete mongoObject._metadata.class_permissions;
} else {
mongoObject._metadata.class_permissions = classLevelPermissions;
}
}
return mongoObject;
}
class MongoSchemaCollection { class MongoSchemaCollection {
_collection: MongoCollection; _collection: MongoCollection;
@@ -136,8 +163,9 @@ class MongoSchemaCollection {
// later PR. Returns a promise that is expected to resolve with the newly created schema, in Parse format. // later PR. Returns a promise that is expected to resolve with the newly created schema, in Parse format.
// If the class already exists, returns a promise that rejects with undefined as the reason. If the collection // If the class already exists, returns a promise that rejects with undefined as the reason. If the collection
// can't be added for a reason other than it already existing, requirements for rejection reason are TBD. // can't be added for a reason other than it already existing, requirements for rejection reason are TBD.
addSchema(name: string, fields) { addSchema(name: string, fields, classLevelPermissions) {
let mongoObject = _mongoSchemaObjectFromNameFields(name, fields); let mongoSchema = mongoSchemaFromFieldsAndClassNameAndCLP(fields, name, classLevelPermissions);
let mongoObject = _mongoSchemaObjectFromNameFields(name, mongoSchema);
return this._collection.insertOne(mongoObject) return this._collection.insertOne(mongoObject)
.then(result => mongoSchemaToParseSchema(result.ops[0])) .then(result => mongoSchemaToParseSchema(result.ops[0]))
.catch(error => { .catch(error => {
@@ -155,18 +183,27 @@ class MongoSchemaCollection {
upsertSchema(name: string, query: string, update) { upsertSchema(name: string, query: string, update) {
return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update); return this._collection.upsertOne(_mongoSchemaQueryFromNameQuery(name, query), update);
} }
updateField(className: string, fieldName: string, type: string) {
// 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 = {};
if (typeof type === 'string') {
type = {
type: type
}
}
update[fieldName] = parseFieldTypeToMongoFieldType(type);
update = {'$set': update};
return this.upsertSchema(className, query, update);
}
} }
// Exported for testing reasons and because we haven't moved all mongo schema format // Exported for testing reasons and because we haven't moved all mongo schema format
// related logic into the database adapter yet. // related logic into the database adapter yet.
MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema MongoSchemaCollection._TESTmongoSchemaToParseSchema = mongoSchemaToParseSchema
// 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._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

@@ -90,9 +90,8 @@ DatabaseController.prototype.loadSchema = function(acceptor = returnsTrue) {
DatabaseController.prototype.redirectClassNameForKey = function(className, key) { DatabaseController.prototype.redirectClassNameForKey = function(className, key) {
return this.loadSchema().then((schema) => { return this.loadSchema().then((schema) => {
var t = schema.getExpectedType(className, key); var t = schema.getExpectedType(className, key);
var match = t ? t.match(/^relation<(.*)>$/) : false; if (t.type == 'Relation') {
if (match) { return t.targetClass;
return match[1];
} else { } else {
return className; return className;
} }
@@ -446,11 +445,10 @@ DatabaseController.prototype.reduceInRelation = function(className, query, schem
let promises = Object.keys(query).map((key) => { let promises = Object.keys(query).map((key) => {
if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) { if (query[key] && (query[key]['$in'] || query[key]['$ne'] || query[key]['$nin'] || query[key].__type == 'Pointer')) {
let t = schema.getExpectedType(className, key); let t = schema.getExpectedType(className, key);
let match = t ? t.match(/^relation<(.*)>$/) : false; if (!t || t.type !== 'Relation') {
if (!match) {
return Promise.resolve(query); return Promise.resolve(query);
} }
let relatedClassName = match[1]; let relatedClassName = t.targetClass;
// Build the list of queries // Build the list of queries
let queries = Object.keys(query[key]).map((constraintKey) => { let queries = Object.keys(query[key]).map((constraintKey) => {
let relatedIds; let relatedIds;
@@ -599,7 +597,6 @@ DatabaseController.prototype.find = function(className, query, options = {}) {
if (options.limit) { if (options.limit) {
mongoOptions.limit = options.limit; mongoOptions.limit = options.limit;
} }
let isMaster = !('acl' in options); let isMaster = !('acl' in options);
let aclGroup = options.acl || []; let aclGroup = options.acl || [];
let acceptor = schema => schema.hasKeys(className, keysForQuery(query)) let acceptor = schema => schema.hasKeys(className, keysForQuery(query))

View File

@@ -213,7 +213,6 @@ class Schema {
this._collection = collection; this._collection = collection;
// this.data[className][fieldName] tells you the type of that field, in mongo format // this.data[className][fieldName] tells you the type of that field, in mongo format
// TODO: use Parse format
this.data = {}; this.data = {};
// this.perms[className][operation] tells you the acl-style permissions // this.perms[className][operation] tells you the acl-style permissions
this.perms = {}; this.perms = {};
@@ -229,14 +228,7 @@ class Schema {
...(defaultColumns[schema.className] || {}), ...(defaultColumns[schema.className] || {}),
...schema.fields, ...schema.fields,
} }
// ACL doesn't show up in mongo, it's implicit this.data[schema.className] = parseFormatSchema;
delete parseFormatSchema.ACL;
// createdAt and updatedAt are wacky and have legacy baggage
parseFormatSchema.createdAt = { type: 'String' };
parseFormatSchema.updatedAt = { type: 'String' };
//Necessary because we still use the mongo type internally here :(
this.data[schema.className] = _.mapValues(parseFormatSchema, MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType);
this.perms[schema.className] = schema.classLevelPermissions; this.perms[schema.className] = schema.classLevelPermissions;
}); });
}); });
@@ -249,17 +241,16 @@ class Schema {
// on success, and rejects with an error on fail. Ensure you // on success, and rejects with an error on fail. Ensure you
// have authorization (master key, or client class creation // have authorization (master key, or client class creation
// enabled) before calling this function. // enabled) before calling this function.
addClassIfNotExists(className, fields, classLevelPermissions) { addClassIfNotExists(className, fields = {}, classLevelPermissions) {
if (this.data[className]) { var validationError = this.validateNewClass(className, fields, classLevelPermissions);
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); if (validationError) {
return Promise.reject(validationError);
} }
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions); return this._collection.addSchema(className, fields, classLevelPermissions)
if (!mongoObject.result) { .then(res => {
return Promise.reject(mongoObject); return Promise.resolve(res);
} })
return this._collection.addSchema(className, mongoObject.result)
.catch(error => { .catch(error => {
if (error === undefined) { if (error === undefined) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`); throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
@@ -285,9 +276,9 @@ class Schema {
}); });
let newSchema = buildMergedSchemaObject(existingFields, submittedFields); let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions); let validationError = this.validateSchemaData(className, newSchema, classLevelPermissions);
if (!mongoObject.result) { if (validationError) {
throw new Parse.Error(mongoObject.code, mongoObject.error); throw new Parse.Error(validationError.code, validationError.error);
} }
// Finally we have checked to make sure the request is valid and we can start deleting fields. // Finally we have checked to make sure the request is valid and we can start deleting fields.
@@ -302,12 +293,13 @@ class Schema {
insertedFields.push(fieldName); insertedFields.push(fieldName);
} }
}); });
return Promise.all(deletePromises) // Delete Everything return Promise.all(deletePromises) // Delete Everything
.then(() => this.reloadData()) // Reload our Schema, so we have all the new values .then(() => this.reloadData()) // Reload our Schema, so we have all the new values
.then(() => { .then(() => {
let promises = insertedFields.map(fieldName => { let promises = insertedFields.map(fieldName => {
const mongoType = mongoObject.result[fieldName]; const type = submittedFields[fieldName];
return this.validateField(className, fieldName, mongoType); return this.validateField(className, fieldName, type);
}); });
return Promise.all(promises); return Promise.all(promises);
}) })
@@ -315,7 +307,11 @@ class Schema {
return this.setPermissions(className, classLevelPermissions) return this.setPermissions(className, classLevelPermissions)
}) })
//TODO: Move this logic into the database adapter //TODO: Move this logic into the database adapter
.then(() => MongoSchemaCollection._TESTmongoSchemaToParseSchema(mongoObject.result)); .then(() => {
return { className: className,
fields: this.data[className],
classLevelPermissions: this.perms[className] }
});
} }
@@ -363,6 +359,51 @@ class Schema {
}); });
} }
validateNewClass(className, fields = {}, classLevelPermissions) {
if (this.data[className]) {
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
}
if (!classNameIsValid(className)) {
return {
code: Parse.Error.INVALID_CLASS_NAME,
error: invalidClassNameMessage(className),
};
}
return this.validateSchemaData(className, fields, classLevelPermissions);
}
validateSchemaData(className, fields, classLevelPermissions) {
for (let fieldName in fields) {
if (!fieldNameIsValid(fieldName)) {
return {
code: Parse.Error.INVALID_KEY_NAME,
error: 'invalid field name: ' + fieldName,
};
}
if (!fieldNameIsValidForClass(fieldName, className)) {
return {
code: 136,
error: 'field ' + fieldName + ' cannot be added',
};
}
const error = fieldTypeIsInvalid(fields[fieldName]);
if (error) return { code: error.code, error: error.message };
}
for (let fieldName in defaultColumns[className]) {
fields[fieldName] = defaultColumns[className][fieldName];
}
let geoPoints = Object.keys(fields).filter(key => fields[key] && fields[key].type === 'GeoPoint');
if (geoPoints.length > 1) {
return {
code: Parse.Error.INCORRECT_TYPE,
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
};
}
validateCLP(classLevelPermissions);
}
// Sets the Class-level permissions for a given className, which must exist. // Sets the Class-level permissions for a given className, which must exist.
setPermissions(className, perms) { setPermissions(className, perms) {
if (typeof perms === 'undefined') { if (typeof perms === 'undefined') {
@@ -393,13 +434,17 @@ class Schema {
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]; let expected = this.data[className][fieldName];
if (expected) { if (expected) {
expected = (expected === 'map' ? 'object' : expected); expected = (expected === 'map' ? 'Object' : expected);
if (expected === type) { if (expected.type && type.type
&& expected.type == type.type
&& expected.targetClass == type.targetClass) {
return Promise.resolve(this);
} else if (expected == type || expected.type == type) {
return Promise.resolve(this); return Promise.resolve(this);
} else { } else {
throw new Parse.Error( throw new Parse.Error(
@@ -419,10 +464,10 @@ class Schema {
return Promise.resolve(this); return Promise.resolve(this);
} }
if (type === 'geopoint') { if (type === 'GeoPoint') {
// Make sure there are not other geopoint fields // Make sure there are not other geopoint fields
for (let otherKey in this.data[className]) { for (let otherKey in this.data[className]) {
if (this.data[className][otherKey] === 'geopoint') { if (this.data[className][otherKey].type === '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'); 'there can only be one geopoint field in a class');
@@ -430,15 +475,7 @@ class Schema {
} }
} }
// We don't have this field. Update the schema. return this._collection.updateField(className, fieldName, type).then(() => {
// 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 // The update succeeded. Reload the schema
return this.reloadData(); return this.reloadData();
}, () => { }, () => {
@@ -487,7 +524,7 @@ class Schema {
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`); throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
} }
if (this.data[className][fieldName].startsWith('relation<')) { if (this.data[className][fieldName].type == 'Relation') {
//For relations, drop the _Join table //For relations, drop the _Join table
return database.dropCollection(`_Join:${fieldName}:${className}`) return database.dropCollection(`_Join:${fieldName}:${className}`)
.then(() => { .then(() => {
@@ -504,7 +541,7 @@ class Schema {
// This is necessary to ensure that the data is still gone if they add the same field. // This is necessary to ensure that the data is still gone if they add the same field.
return database.adaptiveCollection(className) return database.adaptiveCollection(className)
.then(collection => { .then(collection => {
let mongoFieldName = this.data[className][fieldName].startsWith('*') ? `_p_${fieldName}` : fieldName; let mongoFieldName = this.data[className][fieldName].type === 'Pointer' ? `_p_${fieldName}` : fieldName;
return collection.updateMany({}, { "$unset": { [mongoFieldName]: null } }); return collection.updateMany({}, { "$unset": { [mongoFieldName]: null } });
}); });
}) })
@@ -524,7 +561,7 @@ class Schema {
continue; continue;
} }
let expected = getType(object[fieldName]); let expected = getType(object[fieldName]);
if (expected === 'geopoint') { if (expected === 'GeoPoint') {
geocount++; geocount++;
} }
if (geocount > 1) { if (geocount > 1) {
@@ -572,7 +609,6 @@ class Schema {
Parse.Error.INCORRECT_TYPE, Parse.Error.INCORRECT_TYPE,
missingColumns[0]+' is required.'); missingColumns[0]+' is required.');
} }
return Promise.resolve(this); return Promise.resolve(this);
} }
@@ -629,13 +665,12 @@ class Schema {
if (this.data && this.data[className]) { if (this.data && this.data[className]) {
let classData = this.data[className]; let classData = this.data[className];
return Object.keys(classData).filter((field) => { return Object.keys(classData).filter((field) => {
return classData[field].startsWith('relation'); return classData[field].type === 'Relation';
}).reduce((memo, field) => { }).reduce((memo, field) => {
let type = classData[field]; let type = classData[field];
let className = type.slice('relation<'.length, type.length - 1);
memo[field] = { memo[field] = {
__type: 'Relation', __type: 'Relation',
className: className className: type.targetClass
}; };
return memo; return memo;
}, {}); }, {});
@@ -650,85 +685,22 @@ function load(collection) {
return schema.reloadData().then(() => schema); return schema.reloadData().then(() => schema);
} }
// Returns { code, error } if invalid, or { result }, an object
// suitable for inserting into _SCHEMA collection, otherwise.
function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) {
if (!classNameIsValid(className)) {
return {
code: Parse.Error.INVALID_CLASS_NAME,
error: invalidClassNameMessage(className),
};
}
for (let fieldName in fields) {
if (!fieldNameIsValid(fieldName)) {
return {
code: Parse.Error.INVALID_KEY_NAME,
error: 'invalid field name: ' + fieldName,
};
}
if (!fieldNameIsValidForClass(fieldName, className)) {
return {
code: 136,
error: 'field ' + fieldName + ' cannot be added',
};
}
const error = fieldTypeIsInvalid(fields[fieldName]);
if (error) return { code: error.code, error: error.message };
}
let mongoObject = {
_id: className,
objectId: 'string',
updatedAt: 'string',
createdAt: 'string'
};
for (let fieldName in defaultColumns[className]) {
mongoObject[fieldName] = MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType(defaultColumns[className][fieldName]);
}
for (let fieldName in fields) {
mongoObject[fieldName] = MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType(fields[fieldName]);
}
let geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
if (geoPoints.length > 1) {
return {
code: Parse.Error.INCORRECT_TYPE,
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
};
}
validateCLP(classLevelPermissions);
if (typeof classLevelPermissions !== 'undefined') {
mongoObject._metadata = mongoObject._metadata || {};
if (!classLevelPermissions) {
delete mongoObject._metadata.class_permissions;
} else {
mongoObject._metadata.class_permissions = classLevelPermissions;
}
}
return { result: mongoObject };
}
// Builds a new schema (in schema API response format) out of an // Builds a new schema (in schema API response format) out of an
// existing mongo schema + a schemas API put request. This response // existing mongo schema + a schemas API put request. This response
// does not include the default fields, as it is intended to be passed // does not include the default fields, as it is intended to be passed
// 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(existingFields, putRequest) {
let newSchema = {}; let newSchema = {};
let sysSchemaField = Object.keys(defaultColumns).indexOf(mongoObject._id) === -1 ? [] : Object.keys(defaultColumns[mongoObject._id]); let sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]);
for (let oldField in mongoObject) { for (let oldField in existingFields) {
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;
} }
let 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] = existingFields[oldField];
} }
} }
} }
@@ -768,9 +740,11 @@ function getType(obj) {
let type = typeof obj; let type = typeof obj;
switch(type) { switch(type) {
case 'boolean': case 'boolean':
return 'Boolean';
case 'string': case 'string':
return 'String';
case 'number': case 'number':
return type; return 'Number';
case 'map': case 'map':
case 'object': case 'object':
if (!obj) { if (!obj) {
@@ -790,25 +764,28 @@ function getType(obj) {
// Returns null if the type is unknown. // Returns null if the type is unknown.
function getObjectType(obj) { function getObjectType(obj) {
if (obj instanceof Array) { if (obj instanceof Array) {
return 'array'; return 'Array';
} }
if (obj.__type){ if (obj.__type){
switch(obj.__type) { switch(obj.__type) {
case 'Pointer' : case 'Pointer' :
if(obj.className) { if(obj.className) {
return '*' + obj.className; return {
type: 'Pointer',
targetClass: obj.className
}
} }
case 'File' : case 'File' :
if(obj.name) { if(obj.name) {
return 'file'; return 'File';
} }
case 'Date' : case 'Date' :
if(obj.iso) { if(obj.iso) {
return 'date'; return 'Date';
} }
case 'GeoPoint' : case 'GeoPoint' :
if(obj.latitude != null && obj.longitude != null) { if(obj.latitude != null && obj.longitude != null) {
return 'geopoint'; return 'GeoPoint';
} }
case 'Bytes' : case 'Bytes' :
if(obj.base64) { if(obj.base64) {
@@ -824,23 +801,26 @@ function getObjectType(obj) {
if (obj.__op) { if (obj.__op) {
switch(obj.__op) { switch(obj.__op) {
case 'Increment': case 'Increment':
return 'number'; return 'Number';
case 'Delete': case 'Delete':
return null; return null;
case 'Add': case 'Add':
case 'AddUnique': case 'AddUnique':
case 'Remove': case 'Remove':
return 'array'; return 'Array';
case 'AddRelation': case 'AddRelation':
case 'RemoveRelation': case 'RemoveRelation':
return 'relation<' + obj.objects[0].className + '>'; return {
type: 'Relation',
targetClass: obj.objects[0].className
}
case 'Batch': case 'Batch':
return getObjectType(obj.ops[0]); return getObjectType(obj.ops[0]);
default: default:
throw 'unexpected op: ' + obj.__op; throw 'unexpected op: ' + obj.__op;
} }
} }
return 'object'; return 'Object';
} }
export { export {

View File

@@ -87,7 +87,6 @@ function del(config, auth, className, objectId) {
// Returns a promise for a {response, status, location} object. // Returns a promise for a {response, status, location} object.
function create(config, auth, className, restObject) { function create(config, auth, className, restObject) {
enforceRoleSecurity('create', className, auth); enforceRoleSecurity('create', className, auth);
var write = new RestWrite(config, auth, className, null, restObject); var write = new RestWrite(config, auth, className, null, restObject);
return write.execute(); return write.execute();
} }

View File

@@ -115,11 +115,11 @@ export function transformKeyValue(schema, className, restKey, restValue, options
if (schema && schema.getExpectedType) { if (schema && schema.getExpectedType) {
expected = schema.getExpectedType(className, key); expected = schema.getExpectedType(className, key);
} }
if ((expected && expected[0] == '*') || if ((expected && expected.type == 'Pointer') ||
(!expected && restValue && restValue.__type == 'Pointer')) { (!expected && restValue && restValue.__type == 'Pointer')) {
key = '_p_' + key; key = '_p_' + key;
} }
var inArray = (expected === 'array'); var inArray = (expected && expected.type === 'Array');
// Handle query constraints // Handle query constraints
if (options.query) { if (options.query) {
@@ -713,7 +713,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
className, newKey); className, newKey);
break; break;
} }
if (expected && expected[0] != '*') { if (expected && expected.type !== 'Pointer') {
log.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key); log.info('transform.js', 'Found a pointer in a non-pointer column, dropping it.', className, key);
break; break;
} }
@@ -721,7 +721,7 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
break; break;
} }
var objData = mongoObject[key].split('$'); var objData = mongoObject[key].split('$');
var newClass = (expected ? expected.substring(1) : objData[0]); var newClass = (expected ? expected.targetClass : objData[0]);
if (objData[0] !== newClass) { if (objData[0] !== newClass) {
throw 'pointer to incorrect className'; throw 'pointer to incorrect className';
} }
@@ -736,11 +736,11 @@ function untransformObject(schema, className, mongoObject, isNestedObject = fals
} else { } else {
var expectedType = schema.getExpectedType(className, key); var expectedType = schema.getExpectedType(className, key);
var value = mongoObject[key]; var value = mongoObject[key];
if (expectedType === 'file' && FileCoder.isValidDatabaseObject(value)) { if (expectedType && expectedType.type === 'File' && FileCoder.isValidDatabaseObject(value)) {
restObject[key] = FileCoder.databaseToJSON(value); restObject[key] = FileCoder.databaseToJSON(value);
break; break;
} }
if (expectedType === 'geopoint' && GeoPointCoder.isValidDatabaseObject(value)) { if (expectedType && expectedType.type === 'GeoPoint' && GeoPointCoder.isValidDatabaseObject(value)) {
restObject[key] = GeoPointCoder.databaseToJSON(value); restObject[key] = GeoPointCoder.databaseToJSON(value);
break; break;
} }