Schema.js database agnostic (#1468)
* Schema.js database agnostic * nits
This commit is contained in:
@@ -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();
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -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;
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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))
|
||||||
|
|||||||
224
src/Schema.js
224
src/Schema.js
@@ -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 {
|
||||||
|
|||||||
@@ -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();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user