BVB Source Codes

parse-server Show SchemaController.js Source code

Return Download parse-server: download SchemaController.js Source code - Download parse-server Source code - Type:.js
  1. // This class handles schema validation, persistence, and modification.
  2. //
  3. // Each individual Schema object should be immutable. The helpers to
  4. // do things with the Schema just return a new schema when the schema
  5. // is changed.
  6. //
  7. // The canonical place to store this Schema is in the database itself,
  8. // in a _SCHEMA collection. This is not the right way to do it for an
  9. // open source framework, but it's backward compatible, so we're
  10. // keeping it this way for now.
  11. //
  12. // In API-handling code, you should only use the Schema class via the
  13. // DatabaseController. This will let us replace the schema logic for
  14. // different databases.
  15. // TODO: hide all schema logic inside the database adapter.
  16. const Parse = require('parse/node').Parse;
  17.  
  18. const defaultColumns = Object.freeze({
  19.   // Contain the default columns for every parse object type (except _Join collection)
  20.   _Default: {
  21.     "objectId":  {type:'String'},
  22.     "createdAt": {type:'Date'},
  23.     "updatedAt": {type:'Date'},
  24.     "ACL":       {type:'ACL'},
  25.   },
  26.   // The additional default columns for the _User collection (in addition to DefaultCols)
  27.   _User: {
  28.     "username":      {type:'String'},
  29.     "password":      {type:'String'},
  30.     "email":         {type:'String'},
  31.     "emailVerified": {type:'Boolean'},
  32.     "authData":      {type:'Object'}
  33.   },
  34.   // The additional default columns for the _Installation collection (in addition to DefaultCols)
  35.   _Installation: {
  36.     "installationId":   {type:'String'},
  37.     "deviceToken":      {type:'String'},
  38.     "channels":         {type:'Array'},
  39.     "deviceType":       {type:'String'},
  40.     "pushType":         {type:'String'},
  41.     "GCMSenderId":      {type:'String'},
  42.     "timeZone":         {type:'String'},
  43.     "localeIdentifier": {type:'String'},
  44.     "badge":            {type:'Number'},
  45.     "appVersion":       {type:'String'},
  46.     "appName":          {type:'String'},
  47.     "appIdentifier":    {type:'String'},
  48.     "parseVersion":     {type:'String'},
  49.   },
  50.   // The additional default columns for the _Role collection (in addition to DefaultCols)
  51.   _Role: {
  52.     "name":  {type:'String'},
  53.     "users": {type:'Relation', targetClass:'_User'},
  54.     "roles": {type:'Relation', targetClass:'_Role'}
  55.   },
  56.   // The additional default columns for the _Session collection (in addition to DefaultCols)
  57.   _Session: {
  58.     "restricted":     {type:'Boolean'},
  59.     "user":           {type:'Pointer', targetClass:'_User'},
  60.     "installationId": {type:'String'},
  61.     "sessionToken":   {type:'String'},
  62.     "expiresAt":      {type:'Date'},
  63.     "createdWith":    {type:'Object'}
  64.   },
  65.   _Product: {
  66.     "productIdentifier":  {type:'String'},
  67.     "download":           {type:'File'},
  68.     "downloadName":       {type:'String'},
  69.     "icon":               {type:'File'},
  70.     "order":              {type:'Number'},
  71.     "title":              {type:'String'},
  72.     "subtitle":           {type:'String'},
  73.   },
  74.   _PushStatus: {
  75.     "pushTime":     {type:'String'},
  76.     "source":       {type:'String'}, // rest or webui
  77.     "query":        {type:'String'}, // the stringified JSON query
  78.     "payload":      {type:'String'}, // the stringified JSON payload,
  79.     "title":        {type:'String'},
  80.     "expiry":       {type:'Number'},
  81.     "status":       {type:'String'},
  82.     "numSent":      {type:'Number'},
  83.     "numFailed":    {type:'Number'},
  84.     "pushHash":     {type:'String'},
  85.     "errorMessage": {type:'Object'},
  86.     "sentPerType":  {type:'Object'},
  87.     "failedPerType":{type:'Object'},
  88.     "count":       {type:'Number'}
  89.   },
  90.   _JobStatus: {
  91.     "jobName":    {type: 'String'},
  92.     "source":     {type: 'String'},
  93.     "status":     {type: 'String'},
  94.     "message":    {type: 'String'},
  95.     "params":     {type: 'Object'}, // params received when calling the job
  96.     "finishedAt": {type: 'Date'}
  97.   },
  98.   _Hooks: {
  99.     "functionName": {type:'String'},
  100.     "className":    {type:'String'},
  101.     "triggerName":  {type:'String'},
  102.     "url":          {type:'String'}
  103.   },
  104.   _GlobalConfig: {
  105.     "objectId": {type: 'String'},
  106.     "params": {type: 'Object'}
  107.   }
  108. });
  109.  
  110. const requiredColumns = Object.freeze({
  111.   _Product: ["productIdentifier", "icon", "order", "title", "subtitle"],
  112.   _Role: ["name", "ACL"]
  113. });
  114.  
  115. const systemClasses = Object.freeze(['_User', '_Installation', '_Role', '_Session', '_Product', '_PushStatus', '_JobStatus']);
  116.  
  117. const volatileClasses = Object.freeze(['_JobStatus', '_PushStatus', '_Hooks', '_GlobalConfig']);
  118.  
  119. // 10 alpha numberic chars + uppercase
  120. const userIdRegex = /^[a-zA-Z0-9]{10}$/;
  121. // Anything that start with role
  122. const roleRegex = /^role:.*/;
  123. // * permission
  124. const publicRegex = /^\*$/
  125.  
  126. const requireAuthenticationRegex = /^requiresAuthentication$/
  127.  
  128. const permissionKeyRegex = Object.freeze([userIdRegex, roleRegex, publicRegex, requireAuthenticationRegex]);
  129.  
  130. function verifyPermissionKey(key) {
  131.   const result = permissionKeyRegex.reduce((isGood, regEx) => {
  132.     isGood = isGood || key.match(regEx) != null;
  133.     return isGood;
  134.   }, false);
  135.   if (!result) {
  136.     throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid key for class level permissions`);
  137.   }
  138. }
  139.  
  140. const CLPValidKeys = Object.freeze(['find', 'count', 'get', 'create', 'update', 'delete', 'addField', 'readUserFields', 'writeUserFields']);
  141. function validateCLP(perms, fields) {
  142.   if (!perms) {
  143.     return;
  144.   }
  145.   Object.keys(perms).forEach((operation) => {
  146.     if (CLPValidKeys.indexOf(operation) == -1) {
  147.       throw new Parse.Error(Parse.Error.INVALID_JSON, `${operation} is not a valid operation for class level permissions`);
  148.     }
  149.  
  150.     if (operation === 'readUserFields' || operation === 'writeUserFields') {
  151.       if (!Array.isArray(perms[operation])) {
  152.         throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perms[operation]}' is not a valid value for class level permissions ${operation}`);
  153.       } else {
  154.         perms[operation].forEach((key) => {
  155.           if (!fields[key] || fields[key].type != 'Pointer' || fields[key].targetClass != '_User') {
  156.             throw new Parse.Error(Parse.Error.INVALID_JSON, `'${key}' is not a valid column for class level pointer permissions ${operation}`);
  157.           }
  158.         });
  159.       }
  160.       return;
  161.     }
  162.  
  163.     Object.keys(perms[operation]).forEach((key) => {
  164.       verifyPermissionKey(key);
  165.       const perm = perms[operation][key];
  166.       if (perm !== true) {
  167.         throw new Parse.Error(Parse.Error.INVALID_JSON, `'${perm}' is not a valid value for class level permissions ${operation}:${key}:${perm}`);
  168.       }
  169.     });
  170.   });
  171. }
  172. const joinClassRegex = /^_Join:[A-Za-z0-9_]+:[A-Za-z0-9_]+/;
  173. const classAndFieldRegex = /^[A-Za-z][A-Za-z0-9_]*$/;
  174. function classNameIsValid(className) {
  175.   // Valid classes must:
  176.   return (
  177.     // Be one of _User, _Installation, _Role, _Session OR
  178.     systemClasses.indexOf(className) > -1 ||
  179.     // Be a join table OR
  180.     joinClassRegex.test(className) ||
  181.     // Include only alpha-numeric and underscores, and not start with an underscore or number
  182.     fieldNameIsValid(className)
  183.   );
  184. }
  185.  
  186. // Valid fields must be alpha-numeric, and not start with an underscore or number
  187. function fieldNameIsValid(fieldName) {
  188.   return classAndFieldRegex.test(fieldName);
  189. }
  190.  
  191. // Checks that it's not trying to clobber one of the default fields of the class.
  192. function fieldNameIsValidForClass(fieldName, className) {
  193.   if (!fieldNameIsValid(fieldName)) {
  194.     return false;
  195.   }
  196.   if (defaultColumns._Default[fieldName]) {
  197.     return false;
  198.   }
  199.   if (defaultColumns[className] && defaultColumns[className][fieldName]) {
  200.     return false;
  201.   }
  202.   return true;
  203. }
  204.  
  205. function invalidClassNameMessage(className) {
  206.   return 'Invalid classname: ' + className + ', classnames can only have alphanumeric characters and _, and must start with an alpha character ';
  207. }
  208.  
  209. const invalidJsonError = new Parse.Error(Parse.Error.INVALID_JSON, "invalid JSON");
  210. const validNonRelationOrPointerTypes = [
  211.   'Number',
  212.   'String',
  213.   'Boolean',
  214.   'Date',
  215.   'Object',
  216.   'Array',
  217.   'GeoPoint',
  218.   'File',
  219.   'Bytes'
  220. ];
  221. // Returns an error suitable for throwing if the type is invalid
  222. const fieldTypeIsInvalid = ({ type, targetClass }) => {
  223.   if (['Pointer', 'Relation'].indexOf(type) >= 0) {
  224.     if (!targetClass) {
  225.       return new Parse.Error(135, `type ${type} needs a class name`);
  226.     } else if (typeof targetClass !== 'string') {
  227.       return invalidJsonError;
  228.     } else if (!classNameIsValid(targetClass)) {
  229.       return new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(targetClass));
  230.     } else {
  231.       return undefined;
  232.     }
  233.   }
  234.   if (typeof type !== 'string') {
  235.     return invalidJsonError;
  236.   }
  237.   if (validNonRelationOrPointerTypes.indexOf(type) < 0) {
  238.     return new Parse.Error(Parse.Error.INCORRECT_TYPE, `invalid field type: ${type}`);
  239.   }
  240.   return undefined;
  241. }
  242.  
  243. const convertSchemaToAdapterSchema = schema => {
  244.   schema = injectDefaultSchema(schema);
  245.   delete schema.fields.ACL;
  246.   schema.fields._rperm = { type: 'Array' };
  247.   schema.fields._wperm = { type: 'Array' };
  248.  
  249.   if (schema.className === '_User') {
  250.     delete schema.fields.password;
  251.     schema.fields._hashed_password = { type: 'String' };
  252.   }
  253.  
  254.   return schema;
  255. }
  256.  
  257. const convertAdapterSchemaToParseSchema = ({...schema}) => {
  258.   delete schema.fields._rperm;
  259.   delete schema.fields._wperm;
  260.  
  261.   schema.fields.ACL = { type: 'ACL' };
  262.  
  263.   if (schema.className === '_User') {
  264.     delete schema.fields.authData; //Auth data is implicit
  265.     delete schema.fields._hashed_password;
  266.     schema.fields.password = { type: 'String' };
  267.   }
  268.  
  269.   return schema;
  270. }
  271.  
  272. const injectDefaultSchema = ({className, fields, classLevelPermissions}) => ({
  273.   className,
  274.   fields: {
  275.     ...defaultColumns._Default,
  276.     ...(defaultColumns[className] || {}),
  277.     ...fields,
  278.   },
  279.   classLevelPermissions,
  280. });
  281.  
  282. const _HooksSchema =  {className: "_Hooks", fields: defaultColumns._Hooks};
  283. const _GlobalConfigSchema = { className: "_GlobalConfig", fields: defaultColumns._GlobalConfig }
  284. const _PushStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
  285.   className: "_PushStatus",
  286.   fields: {},
  287.   classLevelPermissions: {}
  288. }));
  289. const _JobStatusSchema = convertSchemaToAdapterSchema(injectDefaultSchema({
  290.   className: "_JobStatus",
  291.   fields: {},
  292.   classLevelPermissions: {}
  293. }));
  294. const VolatileClassesSchemas = [_HooksSchema, _JobStatusSchema, _PushStatusSchema, _GlobalConfigSchema];
  295.  
  296. const dbTypeMatchesObjectType = (dbType, objectType) => {
  297.   if (dbType.type !== objectType.type) return false;
  298.   if (dbType.targetClass !== objectType.targetClass) return false;
  299.   if (dbType === objectType.type) return true;
  300.   if (dbType.type === objectType.type) return true;
  301.   return false;
  302. }
  303.  
  304. const typeToString = (type) => {
  305.   if (type.targetClass) {
  306.     return `${type.type}<${type.targetClass}>`;
  307.   }
  308.   return `${type.type || type}`;
  309. }
  310.  
  311. // Stores the entire schema of the app in a weird hybrid format somewhere between
  312. // the mongo format and the Parse format. Soon, this will all be Parse format.
  313. export default class SchemaController {
  314.   _dbAdapter;
  315.   data;
  316.   perms;
  317.  
  318.   constructor(databaseAdapter, schemaCache) {
  319.     this._dbAdapter = databaseAdapter;
  320.     this._cache = schemaCache;
  321.     // this.data[className][fieldName] tells you the type of that field, in mongo format
  322.     this.data = {};
  323.     // this.perms[className][operation] tells you the acl-style permissions
  324.     this.perms = {};
  325.   }
  326.  
  327.   reloadData(options = {clearCache: false}) {
  328.     let promise = Promise.resolve();
  329.     if (options.clearCache) {
  330.       promise = promise.then(() => {
  331.         return this._cache.clear();
  332.       });
  333.     }
  334.     if (this.reloadDataPromise && !options.clearCache) {
  335.       return this.reloadDataPromise;
  336.     }
  337.     this.reloadDataPromise = promise.then(() => {
  338.       return this.getAllClasses(options);
  339.     })
  340.     .then(allSchemas => {
  341.       const data = {};
  342.       const perms = {};
  343.       allSchemas.forEach(schema => {
  344.         data[schema.className] = injectDefaultSchema(schema).fields;
  345.         perms[schema.className] = schema.classLevelPermissions;
  346.       });
  347.  
  348.       // Inject the in-memory classes
  349.       volatileClasses.forEach(className => {
  350.         const schema = injectDefaultSchema({ className });
  351.         data[className] = schema.fields;
  352.         perms[className] = schema.classLevelPermissions;
  353.       });
  354.       this.data = data;
  355.       this.perms = perms;
  356.       delete this.reloadDataPromise;
  357.     }, (err) => {
  358.       this.data = {};
  359.       this.perms = {};
  360.       delete this.reloadDataPromise;
  361.       throw err;
  362.     });
  363.     return this.reloadDataPromise;
  364.   }
  365.  
  366.   getAllClasses(options = {clearCache: false}) {
  367.     let promise = Promise.resolve();
  368.     if (options.clearCache) {
  369.       promise = this._cache.clear();
  370.     }
  371.     return promise.then(() => {
  372.       return this._cache.getAllClasses()
  373.     }).then((allClasses) => {
  374.       if (allClasses && allClasses.length && !options.clearCache) {
  375.         return Promise.resolve(allClasses);
  376.       }
  377.       return this._dbAdapter.getAllClasses()
  378.         .then(allSchemas => allSchemas.map(injectDefaultSchema))
  379.         .then(allSchemas => {
  380.           return this._cache.setAllClasses(allSchemas).then(() => {
  381.             return allSchemas;
  382.           });
  383.         })
  384.     });
  385.   }
  386.  
  387.   getOneSchema(className, allowVolatileClasses = false, options = {clearCache: false}) {
  388.     let promise = Promise.resolve();
  389.     if (options.clearCache) {
  390.       promise = this._cache.clear();
  391.     }
  392.     return promise.then(() => {
  393.       if (allowVolatileClasses && volatileClasses.indexOf(className) > -1) {
  394.         return Promise.resolve({
  395.           className,
  396.           fields: this.data[className],
  397.           classLevelPermissions: this.perms[className]
  398.         });
  399.       }
  400.       return this._cache.getOneSchema(className).then((cached) => {
  401.         if (cached && !options.clearCache) {
  402.           return Promise.resolve(cached);
  403.         }
  404.         return this._dbAdapter.getClass(className)
  405.         .then(injectDefaultSchema)
  406.         .then((result) => {
  407.           return this._cache.setOneSchema(className, result).then(() => {
  408.             return result;
  409.           })
  410.         });
  411.       });
  412.     });
  413.   }
  414.  
  415.   // Create a new class that includes the three default fields.
  416.   // ACL is an implicit column that does not get an entry in the
  417.   // _SCHEMAS database. Returns a promise that resolves with the
  418.   // created schema, in mongo format.
  419.   // on success, and rejects with an error on fail. Ensure you
  420.   // have authorization (master key, or client class creation
  421.   // enabled) before calling this function.
  422.   addClassIfNotExists(className, fields = {}, classLevelPermissions) {
  423.     var validationError = this.validateNewClass(className, fields, classLevelPermissions);
  424.     if (validationError) {
  425.       return Promise.reject(validationError);
  426.     }
  427.  
  428.     return this._dbAdapter.createClass(className, convertSchemaToAdapterSchema({ fields, classLevelPermissions, className }))
  429.     .then(convertAdapterSchemaToParseSchema)
  430.     .then((res) => {
  431.       return this._cache.clear().then(() => {
  432.         return Promise.resolve(res);
  433.       });
  434.     })
  435.     .catch(error => {
  436.       if (error && error.code === Parse.Error.DUPLICATE_VALUE) {
  437.         throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
  438.       } else {
  439.         throw error;
  440.       }
  441.     });
  442.   }
  443.  
  444.   updateClass(className, submittedFields, classLevelPermissions, database) {
  445.     return this.getOneSchema(className)
  446.     .then(schema => {
  447.       const existingFields = schema.fields;
  448.       Object.keys(submittedFields).forEach(name => {
  449.         const field = submittedFields[name];
  450.         if (existingFields[name] && field.__op !== 'Delete') {
  451.           throw new Parse.Error(255, `Field ${name} exists, cannot update.`);
  452.         }
  453.         if (!existingFields[name] && field.__op === 'Delete') {
  454.           throw new Parse.Error(255, `Field ${name} does not exist, cannot delete.`);
  455.         }
  456.       });
  457.  
  458.       delete existingFields._rperm;
  459.       delete existingFields._wperm;
  460.       const newSchema = buildMergedSchemaObject(existingFields, submittedFields);
  461.       const validationError = this.validateSchemaData(className, newSchema, classLevelPermissions, Object.keys(existingFields));
  462.       if (validationError) {
  463.         throw new Parse.Error(validationError.code, validationError.error);
  464.       }
  465.  
  466.       // Finally we have checked to make sure the request is valid and we can start deleting fields.
  467.       // Do all deletions first, then a single save to _SCHEMA collection to handle all additions.
  468.       const deletedFields = [];
  469.       const insertedFields = [];
  470.       Object.keys(submittedFields).forEach(fieldName => {
  471.         if (submittedFields[fieldName].__op === 'Delete') {
  472.           deletedFields.push(fieldName);
  473.         } else {
  474.           insertedFields.push(fieldName);
  475.         }
  476.       });
  477.  
  478.       let deletePromise = Promise.resolve();
  479.       if (deletedFields.length > 0) {
  480.         deletePromise = this.deleteFields(deletedFields, className, database);
  481.       }
  482.  
  483.       return deletePromise // Delete Everything
  484.       .then(() => this.reloadData({ clearCache: true })) // Reload our Schema, so we have all the new values
  485.       .then(() => {
  486.         const promises = insertedFields.map(fieldName => {
  487.           const type = submittedFields[fieldName];
  488.           return this.enforceFieldExists(className, fieldName, type);
  489.         });
  490.         return Promise.all(promises);
  491.       })
  492.       .then(() => this.setPermissions(className, classLevelPermissions, newSchema))
  493.       //TODO: Move this logic into the database adapter
  494.       .then(() => ({
  495.         className: className,
  496.         fields: this.data[className],
  497.         classLevelPermissions: this.perms[className]
  498.       }));
  499.     })
  500.     .catch(error => {
  501.       if (error === undefined) {
  502.         throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
  503.       } else {
  504.         throw error;
  505.       }
  506.     })
  507.   }
  508.  
  509.   // Returns a promise that resolves successfully to the new schema
  510.   // object or fails with a reason.
  511.   enforceClassExists(className) {
  512.     if (this.data[className]) {
  513.       return Promise.resolve(this);
  514.     }
  515.     // We don't have this class. Update the schema
  516.     return this.addClassIfNotExists(className)
  517.     // The schema update succeeded. Reload the schema
  518.     .then(() => this.reloadData({ clearCache: true }))
  519.     .catch(() => {
  520.       // The schema update failed. This can be okay - it might
  521.       // have failed because there's a race condition and a different
  522.       // client is making the exact same schema update that we want.
  523.       // So just reload the schema.
  524.       return this.reloadData({ clearCache: true });
  525.     })
  526.     .then(() => {
  527.       // Ensure that the schema now validates
  528.       if (this.data[className]) {
  529.         return this;
  530.       } else {
  531.         throw new Parse.Error(Parse.Error.INVALID_JSON, `Failed to add ${className}`);
  532.       }
  533.     })
  534.     .catch(() => {
  535.       // The schema still doesn't validate. Give up
  536.       throw new Parse.Error(Parse.Error.INVALID_JSON, 'schema class name does not revalidate');
  537.     });
  538.   }
  539.  
  540.   validateNewClass(className, fields = {}, classLevelPermissions) {
  541.     if (this.data[className]) {
  542.       throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} already exists.`);
  543.     }
  544.     if (!classNameIsValid(className)) {
  545.       return {
  546.         code: Parse.Error.INVALID_CLASS_NAME,
  547.         error: invalidClassNameMessage(className),
  548.       };
  549.     }
  550.     return this.validateSchemaData(className, fields, classLevelPermissions, []);
  551.   }
  552.  
  553.   validateSchemaData(className, fields, classLevelPermissions, existingFieldNames) {
  554.     for (const fieldName in fields) {
  555.       if (existingFieldNames.indexOf(fieldName) < 0) {
  556.         if (!fieldNameIsValid(fieldName)) {
  557.           return {
  558.             code: Parse.Error.INVALID_KEY_NAME,
  559.             error: 'invalid field name: ' + fieldName,
  560.           };
  561.         }
  562.         if (!fieldNameIsValidForClass(fieldName, className)) {
  563.           return {
  564.             code: 136,
  565.             error: 'field ' + fieldName + ' cannot be added',
  566.           };
  567.         }
  568.         const error = fieldTypeIsInvalid(fields[fieldName]);
  569.         if (error) return { code: error.code, error: error.message };
  570.       }
  571.     }
  572.  
  573.     for (const fieldName in defaultColumns[className]) {
  574.       fields[fieldName] = defaultColumns[className][fieldName];
  575.     }
  576.  
  577.     const geoPoints = Object.keys(fields).filter(key => fields[key] && fields[key].type === 'GeoPoint');
  578.     if (geoPoints.length > 1) {
  579.       return {
  580.         code: Parse.Error.INCORRECT_TYPE,
  581.         error: 'currently, only one GeoPoint field may exist in an object. Adding ' + geoPoints[1] + ' when ' + geoPoints[0] + ' already exists.',
  582.       };
  583.     }
  584.     validateCLP(classLevelPermissions, fields);
  585.   }
  586.  
  587.   // Sets the Class-level permissions for a given className, which must exist.
  588.   setPermissions(className, perms, newSchema) {
  589.     if (typeof perms === 'undefined') {
  590.       return Promise.resolve();
  591.     }
  592.     validateCLP(perms, newSchema);
  593.     return this._dbAdapter.setClassLevelPermissions(className, perms)
  594.     .then(() => this.reloadData({ clearCache: true }));
  595.   }
  596.  
  597.   // Returns a promise that resolves successfully to the new schema
  598.   // object if the provided className-fieldName-type tuple is valid.
  599.   // The className must already be validated.
  600.   // If 'freeze' is true, refuse to update the schema for this field.
  601.   enforceFieldExists(className, fieldName, type) {
  602.     if (fieldName.indexOf(".") > 0) {
  603.       // subdocument key (x.y) => ok if x is of type 'object'
  604.       fieldName = fieldName.split(".")[ 0 ];
  605.       type = 'Object';
  606.     }
  607.     if (!fieldNameIsValid(fieldName)) {
  608.       throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `Invalid field name: ${fieldName}.`);
  609.     }
  610.  
  611.     // If someone tries to create a new field with null/undefined as the value, return;
  612.     if (!type) {
  613.       return Promise.resolve(this);
  614.     }
  615.  
  616.     return this.reloadData().then(() => {
  617.       const expectedType = this.getExpectedType(className, fieldName);
  618.       if (typeof type === 'string') {
  619.         type = { type };
  620.       }
  621.  
  622.       if (expectedType) {
  623.         if (!dbTypeMatchesObjectType(expectedType, type)) {
  624.           throw new Parse.Error(
  625.             Parse.Error.INCORRECT_TYPE,
  626.             `schema mismatch for ${className}.${fieldName}; expected ${typeToString(expectedType)} but got ${typeToString(type)}`
  627.           );
  628.         }
  629.         return this;
  630.       }
  631.  
  632.       return this._dbAdapter.addFieldIfNotExists(className, fieldName, type).then(() => {
  633.         // The update succeeded. Reload the schema
  634.         return this.reloadData({ clearCache: true });
  635.       }, () => {
  636.         //TODO: introspect the error and only reload if the error is one for which is makes sense to reload
  637.  
  638.         // The update failed. This can be okay - it might have been a race
  639.         // condition where another client updated the schema in the same
  640.         // way that we wanted to. So, just reload the schema
  641.         return this.reloadData({ clearCache: true });
  642.       }).then(() => {
  643.         // Ensure that the schema now validates
  644.         if (!dbTypeMatchesObjectType(this.getExpectedType(className, fieldName), type)) {
  645.           throw new Parse.Error(Parse.Error.INVALID_JSON, `Could not add field ${fieldName}`);
  646.         }
  647.         // Remove the cached schema
  648.         this._cache.clear();
  649.         return this;
  650.       });
  651.     });
  652.   }
  653.  
  654.   // maintain compatibility
  655.   deleteField(fieldName, className, database) {
  656.     return this.deleteFields([fieldName], className, database);
  657.   }
  658.  
  659.   // Delete fields, and remove that data from all objects. This is intended
  660.   // to remove unused fields, if other writers are writing objects that include
  661.   // this field, the field may reappear. Returns a Promise that resolves with
  662.   // no object on success, or rejects with { code, error } on failure.
  663.   // Passing the database and prefix is necessary in order to drop relation collections
  664.   // and remove fields from objects. Ideally the database would belong to
  665.   // a database adapter and this function would close over it or access it via member.
  666.   deleteFields(fieldNames, className, database) {
  667.     if (!classNameIsValid(className)) {
  668.       throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, invalidClassNameMessage(className));
  669.     }
  670.  
  671.     fieldNames.forEach(fieldName => {
  672.       if (!fieldNameIsValid(fieldName)) {
  673.         throw new Parse.Error(Parse.Error.INVALID_KEY_NAME, `invalid field name: ${fieldName}`);
  674.       }
  675.       //Don't allow deleting the default fields.
  676.       if (!fieldNameIsValidForClass(fieldName, className)) {
  677.         throw new Parse.Error(136, `field ${fieldName} cannot be changed`);
  678.       }
  679.     });
  680.  
  681.     return this.getOneSchema(className, false, {clearCache: true})
  682.     .catch(error => {
  683.       if (error === undefined) {
  684.         throw new Parse.Error(Parse.Error.INVALID_CLASS_NAME, `Class ${className} does not exist.`);
  685.       } else {
  686.         throw error;
  687.       }
  688.     })
  689.     .then(schema => {
  690.       fieldNames.forEach(fieldName => {
  691.         if (!schema.fields[fieldName]) {
  692.           throw new Parse.Error(255, `Field ${fieldName} does not exist, cannot delete.`);
  693.         }
  694.       });
  695.  
  696.       const schemaFields = { ...schema.fields };
  697.       return database.adapter.deleteFields(className, schema, fieldNames)
  698.         .then(() => {
  699.           return Promise.all(fieldNames.map(fieldName => {
  700.             const field = schemaFields[fieldName];
  701.             if (field && field.type === 'Relation') {
  702.               //For relations, drop the _Join table
  703.               return database.adapter.deleteClass(`_Join:${fieldName}:${className}`);
  704.             }
  705.             return Promise.resolve();
  706.           }));
  707.         });
  708.     }).then(() => {
  709.       this._cache.clear();
  710.     });
  711.   }
  712.  
  713.   // Validates an object provided in REST format.
  714.   // Returns a promise that resolves to the new schema if this object is
  715.   // valid.
  716.   validateObject(className, object, query) {
  717.     let geocount = 0;
  718.     let promise = this.enforceClassExists(className);
  719.     for (const fieldName in object) {
  720.       if (object[fieldName] === undefined) {
  721.         continue;
  722.       }
  723.       const expected = getType(object[fieldName]);
  724.       if (expected === 'GeoPoint') {
  725.         geocount++;
  726.       }
  727.       if (geocount > 1) {
  728.         // Make sure all field validation operations run before we return.
  729.         // If not - we are continuing to run logic, but already provided response from the server.
  730.         return promise.then(() => {
  731.           return Promise.reject(new Parse.Error(Parse.Error.INCORRECT_TYPE,
  732.             'there can only be one geopoint field in a class'));
  733.         });
  734.       }
  735.       if (!expected) {
  736.         continue;
  737.       }
  738.       if (fieldName === 'ACL') {
  739.         // Every object has ACL implicitly.
  740.         continue;
  741.       }
  742.  
  743.       promise = promise.then(schema => schema.enforceFieldExists(className, fieldName, expected));
  744.     }
  745.     promise = thenValidateRequiredColumns(promise, className, object, query);
  746.     return promise;
  747.   }
  748.  
  749.   // Validates that all the properties are set for the object
  750.   validateRequiredColumns(className, object, query) {
  751.     const columns = requiredColumns[className];
  752.     if (!columns || columns.length == 0) {
  753.       return Promise.resolve(this);
  754.     }
  755.  
  756.     const missingColumns = columns.filter(function(column){
  757.       if (query && query.objectId) {
  758.         if (object[column] && typeof object[column] === "object") {
  759.           // Trying to delete a required column
  760.           return object[column].__op == 'Delete';
  761.         }
  762.         // Not trying to do anything there
  763.         return false;
  764.       }
  765.       return !object[column]
  766.     });
  767.  
  768.     if (missingColumns.length > 0) {
  769.       throw new Parse.Error(
  770.         Parse.Error.INCORRECT_TYPE,
  771.         missingColumns[0] + ' is required.');
  772.     }
  773.     return Promise.resolve(this);
  774.   }
  775.  
  776.   // Validates the base CLP for an operation
  777.   testBaseCLP(className, aclGroup, operation) {
  778.     if (!this.perms[className] || !this.perms[className][operation]) {
  779.       return true;
  780.     }
  781.     const classPerms = this.perms[className];
  782.     const perms = classPerms[operation];
  783.     // Handle the public scenario quickly
  784.     if (perms['*']) {
  785.       return true;
  786.     }
  787.     // Check permissions against the aclGroup provided (array of userId/roles)
  788.     if (aclGroup.some(acl => { return perms[acl] === true })) {
  789.       return true;
  790.     }
  791.     return false;
  792.   }
  793.  
  794.   // Validates an operation passes class-level-permissions set in the schema
  795.   validatePermission(className, aclGroup, operation) {
  796.     if (this.testBaseCLP(className, aclGroup, operation)) {
  797.       return Promise.resolve();
  798.     }
  799.  
  800.     if (!this.perms[className] || !this.perms[className][operation]) {
  801.       return true;
  802.     }
  803.     const classPerms = this.perms[className];
  804.     const perms = classPerms[operation];
  805.  
  806.     // If only for authenticated users
  807.     // make sure we have an aclGroup
  808.     if (perms['requiresAuthentication']) {
  809.       // If aclGroup has * (public)
  810.       if (!aclGroup || aclGroup.length == 0) {
  811.         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
  812.         'Permission denied, user needs to be authenticated.');
  813.       } else if (aclGroup.indexOf('*') > -1 && aclGroup.length == 1) {
  814.         throw new Parse.Error(Parse.Error.OBJECT_NOT_FOUND,
  815.         'Permission denied, user needs to be authenticated.');
  816.       }
  817.       // requiresAuthentication passed, just move forward
  818.       // probably would be wise at some point to rename to 'authenticatedUser'
  819.       return Promise.resolve();
  820.     }
  821.  
  822.     // No matching CLP, let's check the Pointer permissions
  823.     // And handle those later
  824.     const permissionField = ['get', 'find', 'count'].indexOf(operation) > -1 ? 'readUserFields' : 'writeUserFields';
  825.  
  826.     // Reject create when write lockdown
  827.     if (permissionField == 'writeUserFields' && operation == 'create') {
  828.       throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
  829.         `Permission denied for action ${operation} on class ${className}.`);
  830.     }
  831.  
  832.     // Process the readUserFields later
  833.     if (Array.isArray(classPerms[permissionField]) && classPerms[permissionField].length > 0) {
  834.       return Promise.resolve();
  835.     }
  836.     throw new Parse.Error(Parse.Error.OPERATION_FORBIDDEN,
  837.         `Permission denied for action ${operation} on class ${className}.`);
  838.   }
  839.  
  840.   // Returns the expected type for a className+key combination
  841.   // or undefined if the schema is not set
  842.   getExpectedType(className, fieldName) {
  843.     if (this.data && this.data[className]) {
  844.       const expectedType = this.data[className][fieldName]
  845.       return expectedType === 'map' ? 'Object' : expectedType;
  846.     }
  847.     return undefined;
  848.   }
  849.  
  850.   // Checks if a given class is in the schema.
  851.   hasClass(className) {
  852.     return this.reloadData().then(() => !!(this.data[className]));
  853.   }
  854. }
  855.  
  856. // Returns a promise for a new Schema.
  857. const load = (dbAdapter, schemaCache, options) => {
  858.   const schema = new SchemaController(dbAdapter, schemaCache);
  859.   return schema.reloadData(options).then(() => schema);
  860. }
  861.  
  862. // Builds a new schema (in schema API response format) out of an
  863. // existing mongo schema + a schemas API put request. This response
  864. // does not include the default fields, as it is intended to be passed
  865. // to mongoSchemaFromFieldsAndClassName. No validation is done here, it
  866. // is done in mongoSchemaFromFieldsAndClassName.
  867. function buildMergedSchemaObject(existingFields, putRequest) {
  868.   const newSchema = {};
  869.   const sysSchemaField = Object.keys(defaultColumns).indexOf(existingFields._id) === -1 ? [] : Object.keys(defaultColumns[existingFields._id]);
  870.   for (const oldField in existingFields) {
  871.     if (oldField !== '_id' && oldField !== 'ACL' &&  oldField !== 'updatedAt' && oldField !== 'createdAt' && oldField !== 'objectId') {
  872.       if (sysSchemaField.length > 0 && sysSchemaField.indexOf(oldField) !== -1) {
  873.         continue;
  874.       }
  875.       const fieldIsDeleted = putRequest[oldField] && putRequest[oldField].__op === 'Delete'
  876.       if (!fieldIsDeleted) {
  877.         newSchema[oldField] = existingFields[oldField];
  878.       }
  879.     }
  880.   }
  881.   for (const newField in putRequest) {
  882.     if (newField !== 'objectId' && putRequest[newField].__op !== 'Delete') {
  883.       if (sysSchemaField.length > 0 && sysSchemaField.indexOf(newField) !== -1) {
  884.         continue;
  885.       }
  886.       newSchema[newField] = putRequest[newField];
  887.     }
  888.   }
  889.   return newSchema;
  890. }
  891.  
  892. // Given a schema promise, construct another schema promise that
  893. // validates this field once the schema loads.
  894. function thenValidateRequiredColumns(schemaPromise, className, object, query) {
  895.   return schemaPromise.then((schema) => {
  896.     return schema.validateRequiredColumns(className, object, query);
  897.   });
  898. }
  899.  
  900. // Gets the type from a REST API formatted object, where 'type' is
  901. // extended past javascript types to include the rest of the Parse
  902. // type system.
  903. // The output should be a valid schema value.
  904. // TODO: ensure that this is compatible with the format used in Open DB
  905. function getType(obj) {
  906.   const type = typeof obj;
  907.   switch(type) {
  908.   case 'boolean':
  909.     return 'Boolean';
  910.   case 'string':
  911.     return 'String';
  912.   case 'number':
  913.     return 'Number';
  914.   case 'map':
  915.   case 'object':
  916.     if (!obj) {
  917.       return undefined;
  918.     }
  919.     return getObjectType(obj);
  920.   case 'function':
  921.   case 'symbol':
  922.   case 'undefined':
  923.   default:
  924.     throw 'bad obj: ' + obj;
  925.   }
  926. }
  927.  
  928. // This gets the type for non-JSON types like pointers and files, but
  929. // also gets the appropriate type for $ operators.
  930. // Returns null if the type is unknown.
  931. function getObjectType(obj) {
  932.   if (obj instanceof Array) {
  933.     return 'Array';
  934.   }
  935.   if (obj.__type){
  936.     switch(obj.__type) {
  937.     case 'Pointer' :
  938.       if(obj.className) {
  939.         return {
  940.           type: 'Pointer',
  941.           targetClass: obj.className
  942.         }
  943.       }
  944.       break;
  945.     case 'Relation' :
  946.       if(obj.className) {
  947.         return {
  948.           type: 'Relation',
  949.           targetClass: obj.className
  950.         }
  951.       }
  952.       break;
  953.     case 'File' :
  954.       if(obj.name) {
  955.         return 'File';
  956.       }
  957.       break;
  958.     case 'Date' :
  959.       if(obj.iso) {
  960.         return 'Date';
  961.       }
  962.       break;
  963.     case 'GeoPoint' :
  964.       if(obj.latitude != null && obj.longitude != null) {
  965.         return 'GeoPoint';
  966.       }
  967.       break;
  968.     case 'Bytes' :
  969.       if(obj.base64) {
  970.         return 'Bytes';
  971.       }
  972.       break;
  973.     }
  974.     throw new Parse.Error(Parse.Error.INCORRECT_TYPE, "This is not a valid " + obj.__type);
  975.   }
  976.   if (obj['$ne']) {
  977.     return getObjectType(obj['$ne']);
  978.   }
  979.   if (obj.__op) {
  980.     switch(obj.__op) {
  981.     case 'Increment':
  982.       return 'Number';
  983.     case 'Delete':
  984.       return null;
  985.     case 'Add':
  986.     case 'AddUnique':
  987.     case 'Remove':
  988.       return 'Array';
  989.     case 'AddRelation':
  990.     case 'RemoveRelation':
  991.       return {
  992.         type: 'Relation',
  993.         targetClass: obj.objects[0].className
  994.       }
  995.     case 'Batch':
  996.       return getObjectType(obj.ops[0]);
  997.     default:
  998.       throw 'unexpected op: ' + obj.__op;
  999.     }
  1000.   }
  1001.   return 'Object';
  1002. }
  1003.  
  1004. export {
  1005.   load,
  1006.   classNameIsValid,
  1007.   fieldNameIsValid,
  1008.   invalidClassNameMessage,
  1009.   buildMergedSchemaObject,
  1010.   systemClasses,
  1011.   defaultColumns,
  1012.   convertSchemaToAdapterSchema,
  1013.   VolatileClassesSchemas,
  1014. };
  1015.  
downloadSchemaController.js Source code - Download parse-server Source code
Related Source Codes/Software:
react-boilerplate - 2017-06-07
webtorrent - Streaming torrent client for the web ... 2017-06-06
machine-learning-for-software-engineers - A complete daily plan for studying to become a mac... 2017-06-06
upterm - A terminal emulator for the 21st century. 2017-06-06
lottie-android - Render After Effects animations natively on Androi... 2017-06-07
AsyncDisplayKit - Smooth asynchronous user interfaces for iOS apps. ... 2017-06-07
ionicons - The premium icon font for Ionic ... 2017-06-07
storybook - 2017-06-07
prettier - Prettier is an opinionated JavaScript formatter. ... 2017-06-08
CRYENGINE - CRYENGINE is a powerful real-time game development... 2017-06-11
postal - 2017-06-11
reactide - Reactide is the first dedicated IDE for React web ... 2017-06-11
rkt - rkt is a pod-native container engine for Linux. It... 2017-06-11
uWebSockets - Tiny WebSockets https://for... 2017-06-11
realworld - TodoMVC for the RealWorld - Exemplary fullstack Me... 2017-06-11
goreplay - GoReplay is an open-source tool for capturing and ... 2017-06-10
pyenv - Simple Python version management 2017-06-10
redux-saga - An alternative side effect model for Redux apps ... 2017-06-10
angular-starter - 2017-06-10

 Back to top