2016-01-28 10:58:12 -08:00
|
|
|
|
// This class handles schema validation, persistence, and modification.
|
|
|
|
|
|
//
|
|
|
|
|
|
// Each individual Schema object should be immutable. The helpers to
|
|
|
|
|
|
// do things with the Schema just return a new schema when the schema
|
|
|
|
|
|
// is changed.
|
|
|
|
|
|
//
|
|
|
|
|
|
// The canonical place to store this Schema is in the database itself,
|
|
|
|
|
|
// in a _SCHEMA collection. This is not the right way to do it for an
|
|
|
|
|
|
// open source framework, but it's backward compatible, so we're
|
|
|
|
|
|
// keeping it this way for now.
|
|
|
|
|
|
//
|
|
|
|
|
|
// In API-handling code, you should only use the Schema class via the
|
2016-02-27 02:02:33 -08:00
|
|
|
|
// DatabaseController. This will let us replace the schema logic for
|
2016-01-28 10:58:12 -08:00
|
|
|
|
// different databases.
|
|
|
|
|
|
// TODO: hide all schema logic inside the database adapter.
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
const Parse = require('parse/node').Parse;
|
|
|
|
|
|
const transform = require('./transform');
|
2016-04-05 21:16:39 -07:00
|
|
|
|
import MongoSchemaCollection from './Adapters/Storage/Mongo/MongoSchemaCollection';
|
|
|
|
|
|
import _ from 'lodash';
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2016-03-22 08:12:58 -04:00
|
|
|
|
const defaultColumns = Object.freeze({
|
2016-02-05 09:42:35 -08:00
|
|
|
|
// Contain the default columns for every parse object type (except _Join collection)
|
|
|
|
|
|
_Default: {
|
|
|
|
|
|
"objectId": {type:'String'},
|
|
|
|
|
|
"createdAt": {type:'Date'},
|
|
|
|
|
|
"updatedAt": {type:'Date'},
|
|
|
|
|
|
"ACL": {type:'ACL'},
|
|
|
|
|
|
},
|
|
|
|
|
|
// The additional default columns for the _User collection (in addition to DefaultCols)
|
|
|
|
|
|
_User: {
|
|
|
|
|
|
"username": {type:'String'},
|
|
|
|
|
|
"password": {type:'String'},
|
|
|
|
|
|
"authData": {type:'Object'},
|
|
|
|
|
|
"email": {type:'String'},
|
|
|
|
|
|
"emailVerified": {type:'Boolean'},
|
|
|
|
|
|
},
|
|
|
|
|
|
// The additional default columns for the _User collection (in addition to DefaultCols)
|
|
|
|
|
|
_Installation: {
|
|
|
|
|
|
"installationId": {type:'String'},
|
|
|
|
|
|
"deviceToken": {type:'String'},
|
|
|
|
|
|
"channels": {type:'Array'},
|
|
|
|
|
|
"deviceType": {type:'String'},
|
|
|
|
|
|
"pushType": {type:'String'},
|
|
|
|
|
|
"GCMSenderId": {type:'String'},
|
|
|
|
|
|
"timeZone": {type:'String'},
|
|
|
|
|
|
"localeIdentifier": {type:'String'},
|
2016-02-08 20:19:49 -08:00
|
|
|
|
"badge": {type:'Number'}
|
2016-02-05 09:42:35 -08:00
|
|
|
|
},
|
|
|
|
|
|
// The additional default columns for the _User collection (in addition to DefaultCols)
|
|
|
|
|
|
_Role: {
|
|
|
|
|
|
"name": {type:'String'},
|
2016-03-03 16:37:30 +08:00
|
|
|
|
"users": {type:'Relation', targetClass:'_User'},
|
|
|
|
|
|
"roles": {type:'Relation', targetClass:'_Role'}
|
2016-02-05 09:42:35 -08:00
|
|
|
|
},
|
|
|
|
|
|
// The additional default columns for the _User collection (in addition to DefaultCols)
|
|
|
|
|
|
_Session: {
|
|
|
|
|
|
"restricted": {type:'Boolean'},
|
2016-03-03 16:37:30 +08:00
|
|
|
|
"user": {type:'Pointer', targetClass:'_User'},
|
2016-02-05 09:42:35 -08:00
|
|
|
|
"installationId": {type:'String'},
|
|
|
|
|
|
"sessionToken": {type:'String'},
|
|
|
|
|
|
"expiresAt": {type:'Date'},
|
2016-02-08 20:19:49 -08:00
|
|
|
|
"createdWith": {type:'Object'}
|
2016-02-19 13:06:02 -05:00
|
|
|
|
},
|
|
|
|
|
|
_Product: {
|
|
|
|
|
|
"productIdentifier": {type:'String'},
|
|
|
|
|
|
"download": {type:'File'},
|
|
|
|
|
|
"downloadName": {type:'String'},
|
|
|
|
|
|
"icon": {type:'File'},
|
|
|
|
|
|
"order": {type:'Number'},
|
|
|
|
|
|
"title": {type:'String'},
|
2016-03-12 13:40:59 -05:00
|
|
|
|
"subtitle": {type:'String'},
|
|
|
|
|
|
},
|
|
|
|
|
|
_PushStatus: {
|
|
|
|
|
|
"pushTime": {type:'String'},
|
2016-03-12 14:32:39 -05:00
|
|
|
|
"source": {type:'String'}, // rest or webui
|
2016-03-12 13:40:59 -05:00
|
|
|
|
"query": {type:'String'}, // the stringified JSON query
|
|
|
|
|
|
"payload": {type:'Object'}, // the JSON payload,
|
|
|
|
|
|
"title": {type:'String'},
|
|
|
|
|
|
"expiry": {type:'Number'},
|
|
|
|
|
|
"status": {type:'String'},
|
|
|
|
|
|
"numSent": {type:'Number'},
|
2016-03-13 23:34:44 -04:00
|
|
|
|
"numFailed": {type:'Number'},
|
2016-03-12 13:40:59 -05:00
|
|
|
|
"pushHash": {type:'String'},
|
|
|
|
|
|
"errorMessage": {type:'Object'},
|
2016-03-13 23:34:44 -04:00
|
|
|
|
"sentPerType": {type:'Object'},
|
|
|
|
|
|
"failedPerType":{type:'Object'},
|
2016-02-08 20:19:49 -08:00
|
|
|
|
}
|
2016-03-22 08:12:58 -04:00
|
|
|
|
});
|
2016-02-05 09:42:35 -08:00
|
|
|
|
|
2016-03-22 08:12:58 -04:00
|
|
|
|
const requiredColumns = Object.freeze({
|
2016-03-02 12:21:18 +08:00
|
|
|
|
_Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
|
2016-03-02 16:08:39 +08:00
|
|
|
|
_Role: ["name", "ACL"]
|
2016-03-22 08:12:58 -04:00
|
|
|
|
});
|
2016-02-19 13:06:02 -05:00
|
|
|
|
|
2016-03-22 08:12:58 -04:00
|
|
|
|
const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product']);
|
2016-03-12 13:40:59 -05:00
|
|
|
|
|
2016-03-09 19:58:50 -05:00
|
|
|
|
// 10 alpha numberic chars + uppercase
|
|
|
|
|
|
const userIdRegex = /^[a-zA-Z0-9]{10}$/;
|
|
|
|
|
|
// Anything that start with role
|
|
|
|
|
|
const roleRegex = /^role:.*/;
|
|
|
|
|
|
// * permission
|
|
|
|
|
|
const publicRegex = /^\*$/
|
|
|
|
|
|
|
2016-03-22 08:12:58 -04:00
|
|
|
|
const permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex]);
|
2016-03-09 19:58:50 -05:00
|
|
|
|
|
|
|
|
|
|
function verifyPermissionKey(key) {
|
|
|
|
|
|
let result = permissionKeyRegex.reduce((isGood, regEx) => {
|
|
|
|
|
|
isGood = isGood || key.match(regEx) != null;
|
|
|
|
|
|
return isGood;
|
|
|
|
|
|
}, false);
|
|
|
|
|
|
if (!result) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid key for class level permissions`);
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-03-22 08:12:58 -04:00
|
|
|
|
const CLPValidKeys = Object.freeze(['find', 'get', 'create', 'update', 'delete', 'addField']);
|
2016-03-07 14:49:09 -05:00
|
|
|
|
function validateCLP(perms) {
|
2016-03-07 21:38:20 -05:00
|
|
|
|
if (!perms) {
|
|
|
|
|
|
return;
|
|
|
|
|
|
}
|
2016-03-09 19:58:50 -05:00
|
|
|
|
Object.keys(perms).forEach((operation) => {
|
|
|
|
|
|
if (CLPValidKeys.indexOf(operation) == -1) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
|
2016-03-07 14:49:09 -05:00
|
|
|
|
}
|
2016-03-09 19:58:50 -05:00
|
|
|
|
Object.keys(perms[operation]).forEach((key) => {
|
|
|
|
|
|
verifyPermissionKey(key);
|
|
|
|
|
|
let perm = perms[operation][key];
|
2016-03-10 19:20:05 -05:00
|
|
|
|
if (perm !== true) {
|
2016-03-09 19:58:50 -05:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2016-03-07 14:49:09 -05:00
|
|
|
|
});
|
|
|
|
|
|
}
|
2016-04-12 05:06:00 -07:00
|
|
|
|
const joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
|
|
|
|
|
|
const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
|
2016-02-05 09:42:35 -08:00
|
|
|
|
function classNameIsValid(className) {
|
2016-04-07 04:47:47 -07:00
|
|
|
|
// Valid classes must:
|
|
|
|
|
|
return (
|
|
|
|
|
|
// Be one of _User, _Installation, _Role, _Session OR
|
|
|
|
|
|
systemClasses.indexOf(className) > -1 ||
|
|
|
|
|
|
// Be a join table OR
|
2016-02-05 09:42:35 -08:00
|
|
|
|
joinClassRegex.test(className) ||
|
2016-04-07 04:47:47 -07:00
|
|
|
|
// Include only alpha-numeric and underscores, and not start with an underscore or number
|
2016-02-05 18:53:06 -08:00
|
|
|
|
fieldNameIsValid(className)
|
2016-02-05 09:42:35 -08:00
|
|
|
|
);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Valid fields must be alpha-numeric, and not start with an underscore or number
|
|
|
|
|
|
function fieldNameIsValid(fieldName) {
|
|
|
|
|
|
return classAndFieldRegex.test(fieldName);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Checks that it's not trying to clobber one of the default fields of the class.
|
|
|
|
|
|
function fieldNameIsValidForClass(fieldName, className) {
|
|
|
|
|
|
if (!fieldNameIsValid(fieldName)) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (defaultColumns._Default[fieldName]) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (defaultColumns[className] && defaultColumns[className][fieldName]) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
function invalidClassNameMessage(className) {
|
|
|
|
|
|
return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
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;
|
2016-02-05 09:42:35 -08:00
|
|
|
|
}
|
2016-01-28 10:58:12 -08:00
|
|
|
|
|
2016-04-05 21:16:39 -07:00
|
|
|
|
// Stores the entire schema of the app in a weird hybrid format somewhere between
|
|
|
|
|
|
// the mongo format and the Parse format. Soon, this will all be Parse format.
|
2016-03-04 01:02:44 -08:00
|
|
|
|
class Schema {
|
2016-03-07 20:56:26 -08:00
|
|
|
|
_collection;
|
2016-03-04 01:02:44 -08:00
|
|
|
|
data;
|
|
|
|
|
|
perms;
|
|
|
|
|
|
|
|
|
|
|
|
constructor(collection) {
|
2016-03-07 20:56:26 -08:00
|
|
|
|
this._collection = collection;
|
2016-03-04 01:02:44 -08:00
|
|
|
|
|
2016-04-05 21:16:39 -07:00
|
|
|
|
// this.data[className][fieldName] tells you the type of that field, in mongo format
|
|
|
|
|
|
// TODO: use Parse format
|
2016-03-04 01:02:44 -08:00
|
|
|
|
this.data = {};
|
|
|
|
|
|
// this.perms[className][operation] tells you the acl-style permissions
|
|
|
|
|
|
this.perms = {};
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
reloadData() {
|
|
|
|
|
|
this.data = {};
|
|
|
|
|
|
this.perms = {};
|
2016-04-05 21:16:39 -07:00
|
|
|
|
return this._collection.getAllSchemas().then(allSchemas => {
|
|
|
|
|
|
allSchemas.forEach(schema => {
|
|
|
|
|
|
const parseFormatSchema = {
|
|
|
|
|
|
...defaultColumns._Default,
|
|
|
|
|
|
...(defaultColumns[schema.className] || {}),
|
|
|
|
|
|
...schema.fields,
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
2016-04-05 21:16:39 -07:00
|
|
|
|
// ACL doesn't show up in mongo, it's implicit
|
|
|
|
|
|
delete parseFormatSchema.ACL;
|
|
|
|
|
|
// createdAt and updatedAt are wacky and have legacy baggage
|
|
|
|
|
|
parseFormatSchema.createdAt = { type: 'String' };
|
|
|
|
|
|
parseFormatSchema.updatedAt = { type: 'String' };
|
2016-04-12 05:06:00 -07:00
|
|
|
|
//Necessary because we still use the mongo type internally here :(
|
|
|
|
|
|
this.data[schema.className] = _.mapValues(parseFormatSchema, MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType);
|
2016-04-05 21:16:39 -07:00
|
|
|
|
|
|
|
|
|
|
this.perms[schema.className] = schema.classLevelPermissions;
|
|
|
|
|
|
});
|
2016-03-04 01:02:44 -08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Create a new class that includes the three default fields.
|
|
|
|
|
|
// ACL is an implicit column that does not get an entry in the
|
|
|
|
|
|
// _SCHEMAS database. Returns a promise that resolves with the
|
|
|
|
|
|
// created schema, in mongo format.
|
|
|
|
|
|
// on success, and rejects with an error on fail. Ensure you
|
|
|
|
|
|
// have authorization (master key, or client class creation
|
|
|
|
|
|
// enabled) before calling this function.
|
2016-03-07 21:38:20 -05:00
|
|
|
|
addClassIfNotExists(className, fields, classLevelPermissions) {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (this.data[className]) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-03-09 21:40:11 -05:00
|
|
|
|
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions);
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (!mongoObject.result) {
|
|
|
|
|
|
return Promise.reject(mongoObject);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-03-09 15:21:29 -08:00
|
|
|
|
return this._collection.addSchema(className, mongoObject.result)
|
2016-04-07 04:47:47 -07:00
|
|
|
|
.catch(error => {
|
|
|
|
|
|
if (error === undefined) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INTERNAL_SERVER_ERROR, 'Database adapter error.');
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2016-03-04 01:02:44 -08:00
|
|
|
|
}
|
2016-03-12 13:40:59 -05:00
|
|
|
|
|
2016-03-07 21:38:20 -05:00
|
|
|
|
updateClass(className, submittedFields, classLevelPermissions, database) {
|
|
|
|
|
|
if (!this.data[className]) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
let existingFields = Object.assign(this.data[className], {_id: className});
|
|
|
|
|
|
Object.keys(submittedFields).forEach(name => {
|
|
|
|
|
|
let field = submittedFields[name];
|
|
|
|
|
|
if (existingFields[name] && field.__op !== 'Delete') {
|
|
|
|
|
|
throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!existingFields[name] && field.__op === 'Delete') {
|
|
|
|
|
|
throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
2016-03-12 13:40:59 -05:00
|
|
|
|
|
2016-03-07 21:38:20 -05:00
|
|
|
|
let newSchema = buildMergedSchemaObject(existingFields, submittedFields);
|
2016-03-09 21:40:11 -05:00
|
|
|
|
let mongoObject = mongoSchemaFromFieldsAndClassNameAndCLP(newSchema, className, classLevelPermissions);
|
2016-03-07 21:38:20 -05:00
|
|
|
|
if (!mongoObject.result) {
|
|
|
|
|
|
throw new Parse.Error(mongoObject.code, mongoObject.error);
|
|
|
|
|
|
}
|
2016-03-09 21:40:11 -05:00
|
|
|
|
|
2016-03-07 21:38:20 -05:00
|
|
|
|
// Finally we have checked to make sure the request is valid and we can start deleting fields.
|
|
|
|
|
|
// Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
|
|
|
|
|
|
let deletePromises = [];
|
|
|
|
|
|
let insertedFields = [];
|
|
|
|
|
|
Object.keys(submittedFields).forEach(fieldName => {
|
|
|
|
|
|
if (submittedFields[fieldName].__op === 'Delete') {
|
|
|
|
|
|
const promise = this.deleteField(fieldName, className, database);
|
|
|
|
|
|
deletePromises.push(promise);
|
|
|
|
|
|
} else {
|
|
|
|
|
|
insertedFields.push(fieldName);
|
|
|
|
|
|
}
|
|
|
|
|
|
});
|
|
|
|
|
|
return Promise.all(deletePromises) // Delete Everything
|
|
|
|
|
|
.then(() => this.reloadData()) // Reload our Schema, so we have all the new values
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
let promises = insertedFields.map(fieldName => {
|
|
|
|
|
|
const mongoType = mongoObject.result[fieldName];
|
|
|
|
|
|
return this.validateField(className, fieldName, mongoType);
|
|
|
|
|
|
});
|
|
|
|
|
|
return Promise.all(promises);
|
|
|
|
|
|
})
|
2016-03-12 13:40:59 -05:00
|
|
|
|
.then(() => {
|
2016-03-09 21:40:11 -05:00
|
|
|
|
return this.setPermissions(className, classLevelPermissions)
|
|
|
|
|
|
})
|
2016-04-05 21:16:39 -07:00
|
|
|
|
//TODO: Move this logic into the database adapter
|
|
|
|
|
|
.then(() => MongoSchemaCollection._TESTmongoSchemaToParseSchema(mongoObject.result));
|
2016-03-07 21:38:20 -05:00
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
// Returns whether the schema knows the type of all these keys.
|
|
|
|
|
|
hasKeys(className, keys) {
|
2016-04-12 05:06:00 -07:00
|
|
|
|
for (let key of keys) {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (!this.data[className] || !this.data[className][key]) {
|
|
|
|
|
|
return false;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns a promise that resolves successfully to the new schema
|
|
|
|
|
|
// object or fails with a reason.
|
|
|
|
|
|
// If 'freeze' is true, refuse to update the schema.
|
|
|
|
|
|
// WARNING: this function has side-effects, and doesn't actually
|
|
|
|
|
|
// do any validation of the format of the className. You probably
|
|
|
|
|
|
// should use classNameIsValid or addClassIfNotExists or something
|
|
|
|
|
|
// like that instead. TODO: rename or remove this function.
|
|
|
|
|
|
validateClassName(className, freeze) {
|
|
|
|
|
|
if (this.data[className]) {
|
|
|
|
|
|
return Promise.resolve(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (freeze) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
|
|
|
|
|
'schema is frozen, cannot add: ' + className);
|
|
|
|
|
|
}
|
|
|
|
|
|
// We don't have this class. Update the schema
|
2016-04-05 21:16:39 -07:00
|
|
|
|
return this.addClassIfNotExists(className, []).then(() => {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
// The schema update succeeded. Reload the schema
|
|
|
|
|
|
return this.reloadData();
|
|
|
|
|
|
}, () => {
|
|
|
|
|
|
// The schema update failed. This can be okay - it might
|
|
|
|
|
|
// have failed because there's a race condition and a different
|
|
|
|
|
|
// client is making the exact same schema update that we want.
|
|
|
|
|
|
// So just reload the schema.
|
|
|
|
|
|
return this.reloadData();
|
|
|
|
|
|
}).then(() => {
|
|
|
|
|
|
// Ensure that the schema now validates
|
|
|
|
|
|
return this.validateClassName(className, true);
|
2016-03-07 20:56:26 -08:00
|
|
|
|
}, () => {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
// The schema still doesn't validate. Give up
|
2016-03-07 20:56:26 -08:00
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
|
2016-03-04 01:02:44 -08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Sets the Class-level permissions for a given className, which must exist.
|
|
|
|
|
|
setPermissions(className, perms) {
|
2016-03-09 21:40:11 -05:00
|
|
|
|
if (typeof perms === 'undefined') {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
2016-03-07 14:49:09 -05:00
|
|
|
|
validateCLP(perms);
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let update = {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
_metadata: {
|
|
|
|
|
|
class_permissions: perms
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
update = {'$set': update};
|
2016-03-09 15:21:29 -08:00
|
|
|
|
return this._collection.updateSchema(className, update).then(() => {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
// The update succeeded. Reload the schema
|
|
|
|
|
|
return this.reloadData();
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns a promise that resolves successfully to the new schema
|
2016-04-05 21:16:39 -07:00
|
|
|
|
// object if the provided className-fieldName-type tuple is valid.
|
2016-03-04 01:02:44 -08:00
|
|
|
|
// The className must already be validated.
|
|
|
|
|
|
// If 'freeze' is true, refuse to update the schema for this field.
|
2016-04-05 21:16:39 -07:00
|
|
|
|
validateField(className, fieldName, type, freeze) {
|
2016-04-12 05:06:00 -07:00
|
|
|
|
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';
|
|
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
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}`
|
|
|
|
|
|
);
|
|
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
if (freeze) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON, `schema is frozen, cannot add ${fieldName} field`);
|
|
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
// 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);
|
|
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
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');
|
|
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
// 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
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_JSON,
|
|
|
|
|
|
'schema key will not revalidate');
|
|
|
|
|
|
});
|
2016-03-04 01:02:44 -08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Delete a field, and remove that data from all objects. This is intended
|
|
|
|
|
|
// to remove unused fields, if other writers are writing objects that include
|
|
|
|
|
|
// this field, the field may reappear. Returns a Promise that resolves with
|
|
|
|
|
|
// no object on success, or rejects with { code, error } on failure.
|
|
|
|
|
|
// Passing the database and prefix is necessary in order to drop relation collections
|
|
|
|
|
|
// and remove fields from objects. Ideally the database would belong to
|
|
|
|
|
|
// a database adapter and this function would close over it or access it via member.
|
|
|
|
|
|
deleteField(fieldName, className, database) {
|
|
|
|
|
|
if (!classNameIsValid(className)) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!fieldNameIsValid(fieldName)) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
|
|
|
|
|
|
}
|
|
|
|
|
|
//Don't allow deleting the default fields.
|
|
|
|
|
|
if (!fieldNameIsValidForClass(fieldName, className)) {
|
|
|
|
|
|
throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return this.reloadData()
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
return this.hasClass(className)
|
|
|
|
|
|
.then(hasClass => {
|
|
|
|
|
|
if (!hasClass) {
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (!this.data[className][fieldName]) {
|
|
|
|
|
|
throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if (this.data[className][fieldName].startsWith('relation<')) {
|
|
|
|
|
|
//For relations, drop the _Join table
|
2016-03-07 14:14:28 +08:00
|
|
|
|
return database.dropCollection(`_Join:${fieldName}:${className}`)
|
|
|
|
|
|
.then(() => {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}, error => {
|
|
|
|
|
|
if (error.message == 'ns not found') {
|
|
|
|
|
|
return Promise.resolve();
|
2016-03-06 17:22:36 +08:00
|
|
|
|
}
|
2016-03-07 14:14:28 +08:00
|
|
|
|
return Promise.reject(error);
|
2016-03-06 17:22:36 +08:00
|
|
|
|
});
|
2016-03-04 01:02:44 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// for non-relations, remove all the data.
|
|
|
|
|
|
// This is necessary to ensure that the data is still gone if they add the same field.
|
2016-03-07 20:56:26 -08:00
|
|
|
|
return database.adaptiveCollection(className)
|
2016-03-04 01:02:44 -08:00
|
|
|
|
.then(collection => {
|
2016-03-07 20:56:26 -08:00
|
|
|
|
let mongoFieldName = this.data[className][fieldName].startsWith('*') ? `_p_${fieldName}` : fieldName;
|
|
|
|
|
|
return collection.updateMany({}, { "$unset": { [mongoFieldName]: null } });
|
2016-03-04 01:02:44 -08:00
|
|
|
|
});
|
|
|
|
|
|
})
|
|
|
|
|
|
// Save the _SCHEMA object
|
2016-03-09 15:21:29 -08:00
|
|
|
|
.then(() => this._collection.updateSchema(className, { $unset: { [fieldName]: null } }));
|
2016-03-04 01:02:44 -08:00
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validates an object provided in REST format.
|
|
|
|
|
|
// Returns a promise that resolves to the new schema if this object is
|
|
|
|
|
|
// valid.
|
|
|
|
|
|
validateObject(className, object, query) {
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let geocount = 0;
|
|
|
|
|
|
let promise = this.validateClassName(className);
|
2016-04-05 21:16:39 -07:00
|
|
|
|
for (let fieldName in object) {
|
|
|
|
|
|
if (object[fieldName] === undefined) {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let expected = getType(object[fieldName]);
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (expected === 'geopoint') {
|
|
|
|
|
|
geocount++;
|
|
|
|
|
|
}
|
|
|
|
|
|
if (geocount > 1) {
|
2016-03-07 22:45:46 -08:00
|
|
|
|
// Make sure all field validation operations run before we return.
|
|
|
|
|
|
// If not - we are continuing to run logic, but already provided response from the server.
|
|
|
|
|
|
return promise.then(() => {
|
|
|
|
|
|
return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE,
|
|
|
|
|
|
'there can only be one geopoint field in a class'));
|
|
|
|
|
|
});
|
2016-03-04 01:02:44 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (!expected) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2016-04-05 21:16:39 -07:00
|
|
|
|
if (fieldName === 'ACL') {
|
|
|
|
|
|
// Every object has ACL implicitly.
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
|
|
|
|
|
promise = thenValidateField(promise, className, fieldName, expected);
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
promise = thenValidateRequiredColumns(promise, className, object, query);
|
|
|
|
|
|
return promise;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validates that all the properties are set for the object
|
|
|
|
|
|
validateRequiredColumns(className, object, query) {
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let columns = requiredColumns[className];
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (!columns || columns.length == 0) {
|
|
|
|
|
|
return Promise.resolve(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let missingColumns = columns.filter(function(column){
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (query && query.objectId) {
|
|
|
|
|
|
if (object[column] && typeof object[column] === "object") {
|
|
|
|
|
|
// Trying to delete a required column
|
|
|
|
|
|
return object[column].__op == 'Delete';
|
|
|
|
|
|
}
|
|
|
|
|
|
// Not trying to do anything there
|
|
|
|
|
|
return false;
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
return !object[column]
|
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
if (missingColumns.length > 0) {
|
|
|
|
|
|
throw new Parse.Error(
|
|
|
|
|
|
Parse.Error.INCORRECT_TYPE,
|
|
|
|
|
|
missingColumns[0]+' is required.');
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
|
|
|
|
|
|
return Promise.resolve(this);
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Validates an operation passes class-level-permissions set in the schema
|
|
|
|
|
|
validatePermission(className, aclGroup, operation) {
|
|
|
|
|
|
if (!this.perms[className] || !this.perms[className][operation]) {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let perms = this.perms[className][operation];
|
2016-03-10 19:20:05 -05:00
|
|
|
|
// Handle the public scenario quickly
|
|
|
|
|
|
if (perms['*']) {
|
|
|
|
|
|
return Promise.resolve();
|
|
|
|
|
|
}
|
|
|
|
|
|
// Check permissions against the aclGroup provided (array of userId/roles)
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let found = false;
|
|
|
|
|
|
for (let i = 0; i < aclGroup.length && !found; i++) {
|
2016-03-10 19:20:05 -05:00
|
|
|
|
if (perms[aclGroup[i]]) {
|
|
|
|
|
|
found = true;
|
2016-03-10 18:02:29 -05:00
|
|
|
|
}
|
2016-03-10 19:20:05 -05:00
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (!found) {
|
|
|
|
|
|
// TODO: Verify correct error code
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
|
|
|
|
|
|
'Permission denied for this action.');
|
|
|
|
|
|
}
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Returns the expected type for a className+key combination
|
|
|
|
|
|
// or undefined if the schema is not set
|
|
|
|
|
|
getExpectedType(className, key) {
|
|
|
|
|
|
if (this.data && this.data[className]) {
|
|
|
|
|
|
return this.data[className][key];
|
|
|
|
|
|
}
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// Checks if a given class is in the schema. Needs to load the
|
|
|
|
|
|
// schema first, which is kinda janky. Hopefully we can refactor
|
|
|
|
|
|
// and make this be a regular value.
|
|
|
|
|
|
hasClass(className) {
|
|
|
|
|
|
return this.reloadData().then(() => !!(this.data[className]));
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
2016-03-04 01:02:44 -08:00
|
|
|
|
|
|
|
|
|
|
// Helper function to check if a field is a pointer, returns true or false.
|
|
|
|
|
|
isPointer(className, key) {
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let expected = this.getExpectedType(className, key);
|
2016-03-04 01:02:44 -08:00
|
|
|
|
if (expected && expected.charAt(0) == '*') {
|
|
|
|
|
|
return true;
|
|
|
|
|
|
}
|
|
|
|
|
|
return false;
|
|
|
|
|
|
};
|
2016-04-08 07:42:02 -04:00
|
|
|
|
|
|
|
|
|
|
getRelationFields(className) {
|
|
|
|
|
|
if (this.data && this.data[className]) {
|
|
|
|
|
|
let classData = this.data[className];
|
|
|
|
|
|
return Object.keys(classData).filter((field) => {
|
|
|
|
|
|
return classData[field].startsWith('relation');
|
|
|
|
|
|
}).reduce((memo, field) => {
|
|
|
|
|
|
let type = classData[field];
|
|
|
|
|
|
let className = type.slice('relation<'.length, type.length - 1);
|
|
|
|
|
|
memo[field] = {
|
|
|
|
|
|
__type: 'Relation',
|
|
|
|
|
|
className: className
|
|
|
|
|
|
};
|
|
|
|
|
|
return memo;
|
|
|
|
|
|
}, {});
|
|
|
|
|
|
}
|
|
|
|
|
|
return {};
|
|
|
|
|
|
}
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Returns a promise for a new Schema.
|
|
|
|
|
|
function load(collection) {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
let schema = new Schema(collection);
|
|
|
|
|
|
return schema.reloadData().then(() => schema);
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-02-16 12:30:30 -08:00
|
|
|
|
// Returns { code, error } if invalid, or { result }, an object
|
2016-04-12 05:06:00 -07:00
|
|
|
|
// suitable for inserting into _SCHEMA collection, otherwise.
|
2016-03-09 21:40:11 -05:00
|
|
|
|
function mongoSchemaFromFieldsAndClassNameAndCLP(fields, className, classLevelPermissions) {
|
2016-02-05 09:42:35 -08:00
|
|
|
|
if (!classNameIsValid(className)) {
|
2016-02-16 12:30:30 -08:00
|
|
|
|
return {
|
2016-02-05 09:42:35 -08:00
|
|
|
|
code: Parse.Error.INVALID_CLASS_NAME,
|
|
|
|
|
|
error: invalidClassNameMessage(className),
|
2016-02-16 12:30:30 -08:00
|
|
|
|
};
|
2016-02-05 09:42:35 -08:00
|
|
|
|
}
|
2016-02-16 12:30:30 -08:00
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
for (let fieldName in fields) {
|
2016-02-05 09:42:35 -08:00
|
|
|
|
if (!fieldNameIsValid(fieldName)) {
|
2016-02-16 12:30:30 -08:00
|
|
|
|
return {
|
2016-02-05 09:42:35 -08:00
|
|
|
|
code: Parse.Error.INVALID_KEY_NAME,
|
|
|
|
|
|
error: 'invalid field name: ' + fieldName,
|
2016-02-16 12:30:30 -08:00
|
|
|
|
};
|
2016-02-05 09:42:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (!fieldNameIsValidForClass(fieldName, className)) {
|
2016-02-16 12:30:30 -08:00
|
|
|
|
return {
|
2016-02-05 09:42:35 -08:00
|
|
|
|
code: 136,
|
|
|
|
|
|
error: 'field ' + fieldName + ' cannot be added',
|
2016-02-16 12:30:30 -08:00
|
|
|
|
};
|
2016-02-05 09:42:35 -08:00
|
|
|
|
}
|
2016-04-12 05:06:00 -07:00
|
|
|
|
const error = fieldTypeIsInvalid(fields[fieldName]);
|
|
|
|
|
|
if (error) return { code: error.code, error: error.message };
|
2016-02-05 09:42:35 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let mongoObject = {
|
2016-02-05 09:42:35 -08:00
|
|
|
|
_id: className,
|
|
|
|
|
|
objectId: 'string',
|
|
|
|
|
|
updatedAt: 'string',
|
2016-02-08 20:19:49 -08:00
|
|
|
|
createdAt: 'string'
|
2016-02-05 11:21:17 -08:00
|
|
|
|
};
|
2016-02-16 12:30:30 -08:00
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
for (let fieldName in defaultColumns[className]) {
|
|
|
|
|
|
mongoObject[fieldName] = MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType(defaultColumns[className][fieldName]);
|
2016-02-05 11:21:17 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
for (let fieldName in fields) {
|
|
|
|
|
|
mongoObject[fieldName] = MongoSchemaCollection._DONOTUSEparseFieldTypeToMongoFieldType(fields[fieldName]);
|
2016-02-05 11:21:17 -08:00
|
|
|
|
}
|
|
|
|
|
|
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let geoPoints = Object.keys(mongoObject).filter(key => mongoObject[key] === 'geopoint');
|
2016-02-05 11:21:17 -08:00
|
|
|
|
if (geoPoints.length > 1) {
|
2016-02-16 12:30:30 -08:00
|
|
|
|
return {
|
2016-02-05 11:21:17 -08:00
|
|
|
|
code: Parse.Error.INCORRECT_TYPE,
|
|
|
|
|
|
error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
|
2016-02-16 12:30:30 -08:00
|
|
|
|
};
|
|
|
|
|
|
}
|
2016-03-12 13:40:59 -05:00
|
|
|
|
|
2016-03-09 21:40:11 -05:00
|
|
|
|
validateCLP(classLevelPermissions);
|
|
|
|
|
|
if (typeof classLevelPermissions !== 'undefined') {
|
|
|
|
|
|
mongoObject._metadata = mongoObject._metadata || {};
|
|
|
|
|
|
if (!classLevelPermissions) {
|
|
|
|
|
|
delete mongoObject._metadata.class_permissions;
|
|
|
|
|
|
} else {
|
|
|
|
|
|
mongoObject._metadata.class_permissions = classLevelPermissions;
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2016-02-16 12:30:30 -08:00
|
|
|
|
|
|
|
|
|
|
return { result: mongoObject };
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// Builds a new schema (in schema API response format) out of an
|
|
|
|
|
|
// existing mongo schema + a schemas API put request. This response
|
|
|
|
|
|
// does not include the default fields, as it is intended to be passed
|
|
|
|
|
|
// to mongoSchemaFromFieldsAndClassName. No validation is done here, it
|
|
|
|
|
|
// is done in mongoSchemaFromFieldsAndClassName.
|
|
|
|
|
|
function buildMergedSchemaObject(mongoObject, putRequest) {
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let newSchema = {};
|
2016-03-04 03:06:53 +08:00
|
|
|
|
let sysSchemaField = Object.keys(defaultColumns).indexOf(mongoObject._id) === -1 ? [] : Object.keys(defaultColumns[mongoObject._id]);
|
2016-04-12 05:06:00 -07:00
|
|
|
|
for (let oldField in mongoObject) {
|
2016-02-16 12:30:30 -08:00
|
|
|
|
if (oldField !== '_id' && oldField !== 'ACL' && oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
|
2016-03-04 03:06:53 +08:00
|
|
|
|
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
|
2016-02-16 12:30:30 -08:00
|
|
|
|
if (!fieldIsDeleted) {
|
2016-04-05 21:16:39 -07:00
|
|
|
|
newSchema[oldField] = MongoSchemaCollection._DONOTUSEmongoFieldToParseSchemaField(mongoObject[oldField]);
|
2016-02-16 12:30:30 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
2016-04-12 05:06:00 -07:00
|
|
|
|
for (let newField in putRequest) {
|
2016-02-16 12:30:30 -08:00
|
|
|
|
if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
|
2016-03-04 03:06:53 +08:00
|
|
|
|
if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) {
|
|
|
|
|
|
continue;
|
|
|
|
|
|
}
|
2016-02-16 12:30:30 -08:00
|
|
|
|
newSchema[newField] = putRequest[newField];
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return newSchema;
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-01-28 10:58:12 -08:00
|
|
|
|
// Given a schema promise, construct another schema promise that
|
|
|
|
|
|
// validates this field once the schema loads.
|
|
|
|
|
|
function thenValidateField(schemaPromise, className, key, type) {
|
|
|
|
|
|
return schemaPromise.then((schema) => {
|
|
|
|
|
|
return schema.validateField(className, key, type);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-02-19 13:06:02 -05:00
|
|
|
|
// Given a schema promise, construct another schema promise that
|
|
|
|
|
|
// validates this field once the schema loads.
|
|
|
|
|
|
function thenValidateRequiredColumns(schemaPromise, className, object, query) {
|
|
|
|
|
|
return schemaPromise.then((schema) => {
|
|
|
|
|
|
return schema.validateRequiredColumns(className, object, query);
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-01-28 10:58:12 -08:00
|
|
|
|
// Gets the type from a REST API formatted object, where 'type' is
|
|
|
|
|
|
// extended past javascript types to include the rest of the Parse
|
|
|
|
|
|
// type system.
|
|
|
|
|
|
// The output should be a valid schema value.
|
|
|
|
|
|
// TODO: ensure that this is compatible with the format used in Open DB
|
|
|
|
|
|
function getType(obj) {
|
2016-04-12 05:06:00 -07:00
|
|
|
|
let type = typeof obj;
|
2016-01-28 10:58:12 -08:00
|
|
|
|
switch(type) {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
case 'boolean':
|
|
|
|
|
|
case 'string':
|
|
|
|
|
|
case 'number':
|
|
|
|
|
|
return type;
|
|
|
|
|
|
case 'map':
|
|
|
|
|
|
case 'object':
|
|
|
|
|
|
if (!obj) {
|
|
|
|
|
|
return undefined;
|
|
|
|
|
|
}
|
|
|
|
|
|
return getObjectType(obj);
|
|
|
|
|
|
case 'function':
|
|
|
|
|
|
case 'symbol':
|
|
|
|
|
|
case 'undefined':
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw 'bad obj: ' + obj;
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// This gets the type for non-JSON types like pointers and files, but
|
|
|
|
|
|
// also gets the appropriate type for $ operators.
|
|
|
|
|
|
// Returns null if the type is unknown.
|
|
|
|
|
|
function getObjectType(obj) {
|
|
|
|
|
|
if (obj instanceof Array) {
|
|
|
|
|
|
return 'array';
|
|
|
|
|
|
}
|
2016-02-26 16:39:27 +08:00
|
|
|
|
if (obj.__type){
|
|
|
|
|
|
switch(obj.__type) {
|
|
|
|
|
|
case 'Pointer' :
|
|
|
|
|
|
if(obj.className) {
|
|
|
|
|
|
return '*' + obj.className;
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'File' :
|
2016-02-27 17:00:52 +08:00
|
|
|
|
if(obj.name) {
|
2016-02-26 16:39:27 +08:00
|
|
|
|
return 'file';
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'Date' :
|
|
|
|
|
|
if(obj.iso) {
|
|
|
|
|
|
return 'date';
|
|
|
|
|
|
}
|
|
|
|
|
|
case 'GeoPoint' :
|
|
|
|
|
|
if(obj.latitude != null && obj.longitude != null) {
|
|
|
|
|
|
return 'geopoint';
|
|
|
|
|
|
}
|
2016-02-26 18:40:53 +08:00
|
|
|
|
case 'Bytes' :
|
2016-03-02 20:00:26 -05:00
|
|
|
|
if(obj.base64) {
|
|
|
|
|
|
return;
|
2016-02-26 18:40:53 +08:00
|
|
|
|
}
|
2016-03-02 20:00:26 -05:00
|
|
|
|
default:
|
|
|
|
|
|
throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid "+obj.__type);
|
2016-02-26 16:39:27 +08:00
|
|
|
|
}
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
|
|
|
|
|
if (obj['$ne']) {
|
|
|
|
|
|
return getObjectType(obj['$ne']);
|
|
|
|
|
|
}
|
|
|
|
|
|
if (obj.__op) {
|
|
|
|
|
|
switch(obj.__op) {
|
2016-03-04 01:02:44 -08:00
|
|
|
|
case 'Increment':
|
|
|
|
|
|
return 'number';
|
|
|
|
|
|
case 'Delete':
|
|
|
|
|
|
return null;
|
|
|
|
|
|
case 'Add':
|
|
|
|
|
|
case 'AddUnique':
|
|
|
|
|
|
case 'Remove':
|
|
|
|
|
|
return 'array';
|
|
|
|
|
|
case 'AddRelation':
|
|
|
|
|
|
case 'RemoveRelation':
|
|
|
|
|
|
return 'relation<' + obj.objects[0].className + '>';
|
|
|
|
|
|
case 'Batch':
|
|
|
|
|
|
return getObjectType(obj.ops[0]);
|
|
|
|
|
|
default:
|
|
|
|
|
|
throw 'unexpected op: ' + obj.__op;
|
2016-01-28 10:58:12 -08:00
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
return 'object';
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2016-03-22 08:12:58 -04:00
|
|
|
|
export {
|
|
|
|
|
|
load,
|
|
|
|
|
|
classNameIsValid,
|
|
|
|
|
|
invalidClassNameMessage,
|
|
|
|
|
|
buildMergedSchemaObject,
|
2016-03-12 13:40:59 -05:00
|
|
|
|
systemClasses,
|
2016-04-07 22:13:07 -04:00
|
|
|
|
defaultColumns,
|
2016-01-28 10:58:12 -08:00
|
|
|
|
};
|