Repository: parse-community/Parse-SDK-JS
Pull request #1282 (2021-01-07 17:46:36)

fix(LiveQuery): Allow connect with null fields
Fixes issue where optional field if null won't connect to the LiveQueryServer.

Fixes: https://github.com/parse-community/parse-server/pull/7099

See [LiveQuery Protocol Specification](https://github.com/parse-community/parse-server/wiki/Parse-LiveQuery-Protocol-Specification)

peril-parse-community[bot]: 
<!--
  0 failure: 
  0 warning: 
  
  1 markdown notices
  DangerID: danger-id-Peril;
-->



Danger run resulted in 1 warning; to find out more, see the [checks page](https://github.com/parse-community/Parse-SDK-JS/runs/1664091503).
<p align="right">
  Generated by :no_entry_sign: <a href="https://danger.systems/js">dangerJS</a>
</p>

codecov[bot]: # [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282?src=pr&el=h1) Report
> Merging [#1282](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282?src=pr&el=desc) (e848f90) into [master](https://codecov.io/gh/parse-community/Parse-SDK-JS/commit/88d4ecd2e2303ce3679e6970201b7a7cf8cca1ff?el=desc) (88d4ecd) will **not change** coverage.
> The diff coverage is `100.00%`.

[![Impacted file tree graph](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282/graphs/tree.svg?width=650&height=150&src=pr&token=8nCENM0QHj)](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282?src=pr&el=tree)

```diff
@@            Coverage Diff            @@
##            master     #1282   +/-   ##
=========================================
  Coverage   100.00%   100.00%           
=========================================
  Files           57        57           
  Lines         5434      5434           
  Branches      1219      1222    +3     
=========================================
  Hits          5434      5434           
```


| [Impacted Files](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282?src=pr&el=tree) | Coverage Δ | |
|---|---|---|
| [src/LiveQueryClient.js](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282/diff?src=pr&el=tree#diff-c3JjL0xpdmVRdWVyeUNsaWVudC5qcw==) | `100.00% <100.00%> (ø)` | |

------

[Continue to review full report at Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282?src=pr&el=continue).
> **Legend** - [Click here to learn more](https://docs.codecov.io/docs/codecov-delta)
> `Δ = absolute <relative> (impact)`, `ø = not affected`, `? = missing data`
> Powered by [Codecov](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282?src=pr&el=footer). Last update [88d4ecd...e848f90](https://codecov.io/gh/parse-community/Parse-SDK-JS/pull/1282?src=pr&el=lastupdated). Read the [comment docs](https://docs.codecov.io/docs/pull-request-comments).


Patch:
diff --git a/src/LiveQueryClient.js b/src/LiveQueryClient.js
index e9a4391..3066ac1 100644
--- a/src/LiveQueryClient.js
+++ b/src/LiveQueryClient.js
@@ -160,10 +160,10 @@ class LiveQueryClient extends EventEmitter {
     this.requestId = 1;
     this.serverURL = serverURL;
     this.applicationId = applicationId;
-    this.javascriptKey = javascriptKey;
-    this.masterKey = masterKey;
+    this.javascriptKey = javascriptKey || undefined;
+    this.masterKey = masterKey || undefined;
     this.sessionToken = sessionToken || undefined;
-    this.installationId = installationId;
+    this.installationId = installationId || undefined;
     this.additionalProperties = true;
     this.connectPromise = resolvingPromise();
     this.subscriptions = new Map();
diff --git a/src/Parse.js b/src/Parse.js
index a627da8..aeef1f2 100644
--- a/src/Parse.js
+++ b/src/Parse.js
@@ -193,6 +193,7 @@ Parse.ACL = require('./ParseACL').default;
 Parse.Analytics = require('./Analytics');
 Parse.AnonymousUtils = require('./AnonymousUtils').default;
 Parse.Cloud = require('./Cloud');
+Parse.CLP = require('./ParseCLP').default;
 Parse.CoreManager = require('./CoreManager');
 Parse.Config = require('./ParseConfig').default;
 Parse.Error = require('./ParseError').default;
diff --git a/src/ParseCLP.js b/src/ParseCLP.js
new file mode 100644
index 0000000..1791328
--- /dev/null
+++ b/src/ParseCLP.js
@@ -0,0 +1,612 @@
+/**
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ *
+ * @flow
+ */
+
+import ParseRole from './ParseRole';
+import ParseUser from './ParseUser';
+
+type Entity = Entity;
+type UsersMap = { [userId: string]: boolean | any };
+export type PermissionsMap = { [permission: string]: UsersMap };
+
+const PUBLIC_KEY = '*';
+
+const VALID_PERMISSIONS: Map<string, UsersMap> = new Map<string, UsersMap>();
+VALID_PERMISSIONS.set('get', {});
+VALID_PERMISSIONS.set('find', {});
+VALID_PERMISSIONS.set('count', {});
+VALID_PERMISSIONS.set('create', {});
+VALID_PERMISSIONS.set('update', {});
+VALID_PERMISSIONS.set('delete', {});
+VALID_PERMISSIONS.set('addField', {});
+
+const VALID_PERMISSIONS_EXTENDED: Map<string, UsersMap> = new Map<string, UsersMap>();
+VALID_PERMISSIONS_EXTENDED.set('protectedFields', {});
+
+/**
+ * Creates a new CLP.
+ * If no argument is given, the CLP has no permissions for anyone.
+ * If the argument is a Parse.User or Parse.Role, the CLP will have read and write
+ *   permission for only that user or role.
+ * If the argument is any other JSON object, that object will be interpretted
+ *   as a serialized CLP created with toJSON().
+ *
+ * <p>A CLP, or Class Level Permissions can be added to any
+ * <code>Parse.Schema</code> to restrict access to only a subset of users
+ * of your application.</p>
+ *
+ * <p>
+ * For get/count/find/create/update/delete/addField using the following functions:
+ *
+ * Entity is type Parse.User or Parse.Role or string
+ * Role is type Parse.Role or Name of Parse.Role
+ *
+ * getGetRequiresAuthentication()
+ * setGetRequiresAuthentication(allowed: boolean)
+ * getGetPointerFields()
+ * setGetPointerFields(pointerFields: string[])
+ * getGetAccess(entity: Entity)
+ * setGetAccess(entity: Entity, allowed: boolean)
+ * getPublicGetAccess()
+ * setPublicGetAccess(allowed: boolean)
+ * getRoleGetAccess(role: Role)
+ * setRoleGetAccess(role: Role, allowed: boolean)
+ * getFindRequiresAuthentication()
+ * setFindRequiresAuthentication(allowed: boolean)
+ * getFindPointerFields()
+ * setFindPointerFields(pointerFields: string[])
+ * getFindAccess(entity: Entity)
+ * setFindAccess(entity: Entity, allowed: boolean)
+ * getPublicFindAccess()
+ * setPublicFindAccess(allowed: boolean)
+ * getRoleFindAccess(role: Role)
+ * setRoleFindAccess(role: Role, allowed: boolean)
+ * getCountRequiresAuthentication()
+ * setCountRequiresAuthentication(allowed: boolean)
+ * getCountPointerFields()
+ * setCountPointerFields(pointerFields: string[])
+ * getCountAccess(entity: Entity)
+ * setCountAccess(entity: Entity, allowed: boolean)
+ * getPublicCountAccess()
+ * setPublicCountAccess(allowed: boolean)
+ * getRoleCountAccess(role: Role)
+ * setRoleCountAccess(role: Role, allowed: boolean)
+ * getCreateRequiresAuthentication()
+ * setCreateRequiresAuthentication(allowed: boolean)
+ * getCreatePointerFields()
+ * setCreatePointerFields(pointerFields: string[])
+ * getCreateAccess(entity: Entity)
+ * setCreateAccess(entity: Entity, allowed: boolean)
+ * getPublicCreateAccess()
+ * setPublicCreateAccess(allowed: Boolean)
+ * getRoleCreateAccess(role: Role)
+ * setRoleCreateAccess(role: Role, allowed: boolean)
+ * getUpdateRequiresAuthentication()
+ * setUpdateRequiresAuthentication(allowed: boolean)
+ * getUpdatePointerFields()
+ * setUpdatePointerFields(pointerFields: string[])
+ * getUpdateAccess(entity: Entity)
+ * setUpdateAccess(entity: Entity, allowed: boolean)
+ * getPublicUpdateAccess()
+ * setPublicUpdateAccess(allowed: boolean)
+ * getRoleUpdateAccess(role: Role)
+ * setRoleUpdateAccess(role: Role, allowed: boolean)
+ * getDeleteRequiresAuthentication()
+ * setDeleteRequiresAuthentication(allowed: boolean)
+ * getDeletePointerFields()
+ * setDeletePointerFields(pointerFields: string[])
+ * getDeleteAccess(entity: Entity)
+ * setDeleteAccess(entity: Entity, allowed: boolean)
+ * getPublicDeleteAccess()
+ * setPublicDeleteAccess(allowed: boolean)
+ * getRoleDeleteAccess(role: Role)
+ * setRoleDeleteAccess(role: Role, allowed: boolean)
+ * getAddFieldRequiresAuthentication()
+ * setAddFieldRequiresAuthentication(allowed: boolean)
+ * getAddFieldPointerFields()
+ * setAddFieldPointerFields(pointerFields: string[])
+ * getAddFieldAccess(entity: Entity)
+ * setAddFieldAccess(entity: Entity, allowed: boolean)
+ * getPublicAddFieldAccess()
+ * setPublicAddFieldAccess(allowed: boolean)
+ * getRoleAddFieldAccess(role: Role)
+ * setRoleAddFieldAccess(role: Role, allowed: boolean)
+ * </p>
+ *
+ * @alias Parse.CLP
+ */
+class ParseCLP {
+  permissionsMap: PermissionsMap;
+
+  /**
+   * @param {(Parse.User | Parse.Role | object)} userId The user to initialize the CLP for
+   */
+  constructor(userId: ParseUser | ParseRole | PermissionsMap) {
+    this.permissionsMap = {};
+    // Initialize permissions Map with default permissions
+    for (const [operation, group] of VALID_PERMISSIONS.entries()) {
+      this.permissionsMap[operation] = Object.assign({}, group);
+      const action = operation.charAt(0).toUpperCase() + operation.slice(1);
+
+      this[`get${action}RequiresAuthentication`] = function () {
+        return this._getAccess(operation, 'requiresAuthentication');
+      };
+      this[`set${action}RequiresAuthentication`] = function (allowed) {
+        this._setAccess(operation, 'requiresAuthentication', allowed);
+      };
+
+      this[`get${action}PointerFields`] = function () {
+        return this._getAccess(operation, 'pointerFields', false);
+      };
+      this[`set${action}PointerFields`] = function (pointerFields) {
+        this._setArrayAccess(operation, 'pointerFields', pointerFields);
+      };
+
+      this[`get${action}Access`] = function (entity) {
+        return this._getAccess(operation, entity);
+      };
+      this[`set${action}Access`] = function (entity, allowed) {
+        this._setAccess(operation, entity, allowed);
+      };
+
+      this[`getPublic${action}Access`] = function () {
+        return this[`get${action}Access`](PUBLIC_KEY);
+      };
+      this[`setPublic${action}Access`] = function (allowed) {
+        this[`set${action}Access`](PUBLIC_KEY, allowed);
+      };
+
+      this[`getRole${action}Access`] = function (role) {
+        return this[`get${action}Access`](this._getRoleName(role));
+      };
+      this[`setRole${action}Access`] = function (role, allowed) {
+        this[`set${action}Access`](this._getRoleName(role), allowed);
+      };
+    }
+    // Initialize permissions Map with default extended permissions
+    for (const [operation, group] of VALID_PERMISSIONS_EXTENDED.entries()) {
+      this.permissionsMap[operation] = Object.assign({}, group);
+    }
+    if (userId && typeof userId === 'object') {
+      if (userId instanceof ParseUser) {
+        this.setReadAccess(userId, true);
+        this.setWriteAccess(userId, true);
+      } else if (userId instanceof ParseRole) {
+        this.setRoleReadAccess(userId, true);
+        this.setRoleWriteAccess(userId, true);
+      } else {
+        for (const permission in userId) {
+          const users = userId[permission];
+          const isValidPermission = !!VALID_PERMISSIONS.get(permission);
+          const isValidPermissionExtended = !!VALID_PERMISSIONS_EXTENDED.get(permission);
+          const isValidGroupPermission = ['readUserFields', 'writeUserFields'].includes(permission);
+          if (
+            typeof permission !== 'string' ||
+            !(isValidPermission || isValidPermissionExtended || isValidGroupPermission)
+          ) {
+            throw new TypeError('Tried to create an CLP with an invalid permission type.');
+          }
+          if (isValidGroupPermission) {
+            if (users.every(pointer => typeof pointer === 'string')) {
+              this.permissionsMap[permission] = users;
+              continue;
+            } else {
+              throw new TypeError('Tried to create an CLP with an invalid permission value.');
+            }
+          }
+          for (const user in users) {
+            const allowed = users[user];
+            if (
+              typeof allowed !== 'boolean' &&
+              !isValidPermissionExtended &&
+              user !== 'pointerFields'
+            ) {
+              throw new TypeError('Tried to create an CLP with an invalid permission value.');
+            }
+            this.permissionsMap[permission][user] = allowed;
+          }
+        }
+      }
+    } else if (typeof userId === 'function') {
+      throw new TypeError('ParseCLP constructed with a function. Did you forget ()?');
+    }
+  }
+
+  /**
+   * Returns a JSON-encoded version of the CLP.
+   *
+   * @returns {object}
+   */
+  toJSON(): PermissionsMap {
+    return { ...this.permissionsMap };
+  }
+
+  /**
+   * Returns whether this CLP is equal to another object
+   *
+   * @param other The other object to compare to
+   * @returns {boolean}
+   */
+  equals(other: ParseCLP): boolean {
+    if (!(other instanceof ParseCLP)) {
+      return false;
+    }
+    const permissions = Object.keys(this.permissionsMap);
+    const otherPermissions = Object.keys(other.permissionsMap);
+    if (permissions.length !== otherPermissions.length) {
+      return false;
+    }
+    for (const permission in this.permissionsMap) {
+      if (!other.permissionsMap[permission]) {
+        return false;
+      }
+
+      const users = Object.keys(this.permissionsMap[permission]);
+      const otherUsers = Object.keys(other.permissionsMap[permission]);
+      if (users.length !== otherUsers.length) {
+        return false;
+      }
+
+      for (const user in this.permissionsMap[permission]) {
+        if (!other.permissionsMap[permission][user]) {
+          return false;
+        }
+        if (this.permissionsMap[permission][user] !== other.permissionsMap[permission][user]) {
+          return false;
+        }
+      }
+    }
+    return true;
+  }
+
+  _getRoleName(role: ParseRole | string): string {
+    let name = role;
+    if (role instanceof ParseRole) {
+      // Normalize to the String name
+      name = role.getName();
+    }
+    if (typeof name !== 'string') {
+      throw new TypeError('role must be a Parse.Role or a String');
+    }
+    return `role:${name}`;
+  }
+
+  _parseEntity(entity: Entity) {
+    let userId = entity;
+    if (userId instanceof ParseUser) {
+      userId = userId.id;
+      if (!userId) {
+        throw new Error('Cannot get access for a Parse.User without an id.');
+      }
+    } else if (userId instanceof ParseRole) {
+      userId = this._getRoleName(userId);
+    }
+    if (typeof userId !== 'string') {
+      throw new TypeError('userId must be a string.');
+    }
+    return userId;
+  }
+
+  _setAccess(permission: string, userId: Entity, allowed: boolean) {
+    userId = this._parseEntity(userId);
+
+    if (typeof allowed !== 'boolean') {
+      throw new TypeError('allowed must be either true or false.');
+    }
+    const permissions = this.permissionsMap[permission][userId];
+    if (!permissions) {
+      if (!allowed) {
+        // The user already doesn't have this permission, so no action is needed
+        return;
+      } else {
+        this.permissionsMap[permission][userId] = {};
+      }
+    }
+
+    if (allowed) {
+      this.permissionsMap[permission][userId] = true;
+    } else {
+      delete this.permissionsMap[permission][userId];
+    }
+  }
+
+  _getAccess(permission: string, userId: Entity, returnBoolean = true): boolean | string[] {
+    userId = this._parseEntity(userId);
+
+    const permissions = this.permissionsMap[permission][userId];
+    if (returnBoolean) {
+      if (!permissions) {
+        return false;
+      }
+      return !!this.permissionsMap[permission][userId];
+    }
+    return permissions;
+  }
+
+  _setArrayAccess(permission: string, userId: Entity, fields: string) {
+    userId = this._parseEntity(userId);
+
+    const permissions = this.permissionsMap[permission][userId];
+    if (!permissions) {
+      this.permissionsMap[permission][userId] = [];
+    }
+    if (!fields || (Array.isArray(fields) && fields.length === 0)) {
+      delete this.permissionsMap[permission][userId];
+    } else if (Array.isArray(fields) && fields.every(field => typeof field === 'string')) {
+      this.permissionsMap[permission][userId] = fields;
+    } else {
+      throw new TypeError('fields must be an array of strings or undefined.');
+    }
+  }
+
+  _setGroupPointerPermission(operation: string, pointerFields: string[]) {
+    const fields = this.permissionsMap[operation];
+    if (!fields) {
+      this.permissionsMap[operation] = [];
+    }
+    if (!pointerFields || (Array.isArray(pointerFields) && pointerFields.length === 0)) {
+      delete this.permissionsMap[operation];
+    } else if (
+      Array.isArray(pointerFields) &&
+      pointerFields.every(field => typeof field === 'string')
+    ) {
+      this.permissionsMap[operation] = pointerFields;
+    } else {
+      throw new TypeError(`${operation}.pointerFields must be an array of strings or undefined.`);
+    }
+  }
+
+  _getGroupPointerPermissions(operation: string): string[] {
+    return this.permissionsMap[operation];
+  }
+
+  /**
+   * Sets user pointer fields to allow permission for get/count/find operations.
+   *
+   * @param {string[]} pointerFields User pointer fields
+   */
+  setReadUserFields(pointerFields: string[]) {
+    this._setGroupPointerPermission('readUserFields', pointerFields);
+  }
+
+  /**
+   * @returns {string[]} User pointer fields
+   */
+  getReadUserFields(): string[] {
+    return this._getGroupPointerPermissions('readUserFields');
+  }
+
+  /**
+   * Sets user pointer fields to allow permission for create/delete/update/addField operations
+   *
+   * @param {string[]} pointerFields User pointer fields
+   */
+  setWriteUserFields(pointerFields: string[]) {
+    this._setGroupPointerPermission('writeUserFields', pointerFields);
+  }
+
+  /**
+   * @returns {string[]} User pointer fields
+   */
+  getWriteUserFields(): string[] {
+    return this._getGroupPointerPermissions('writeUserFields');
+  }
+
+  /**
+   * Sets whether the given user is allowed to retrieve fields from this class.
+   *
+   * @param userId An instance of Parse.User or its objectId.
+   * @param {string[]} fields fields to be protected
+   */
+  setProtectedFields(userId: Entity, fields: string[]) {
+    this._setArrayAccess('protectedFields', userId, fields);
+  }
+
+  /**
+   * Returns array of fields are accessable to this user.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role.
+   * @returns {string[]}
+   */
+  getProtectedFields(userId: Entity): string[] {
+    return this._getAccess('protectedFields', userId, false);
+  }
+
+  /**
+   * Sets whether the given user is allowed to read from this class.
+   *
+   * @param userId An instance of Parse.User or its objectId.
+   * @param {boolean} allowed whether that user should have read access.
+   */
+  setReadAccess(userId: Entity, allowed: boolean) {
+    this._setAccess('find', userId, allowed);
+    this._setAccess('get', userId, allowed);
+    this._setAccess('count', userId, allowed);
+  }
+
+  /**
+   * Get whether the given user id is *explicitly* allowed to read from this class.
+   * Even if this returns false, the user may still be able to access it if
+   * getPublicReadAccess returns true or a role that the user belongs to has
+   * write access.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role.
+   * @returns {boolean}
+   */
+  getReadAccess(userId: Entity): boolean {
+    return (
+      this._getAccess('find', userId) &&
+      this._getAccess('get', userId) &&
+      this._getAccess('count', userId)
+    );
+  }
+
+  /**
+   * Sets whether the given user id is allowed to write to this class.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role..
+   * @param {boolean} allowed Whether that user should have write access.
+   */
+  setWriteAccess(userId: Entity, allowed: boolean) {
+    this._setAccess('create', userId, allowed);
+    this._setAccess('update', userId, allowed);
+    this._setAccess('delete', userId, allowed);
+    this._setAccess('addField', userId, allowed);
+  }
+
+  /**
+   * Gets whether the given user id is *explicitly* allowed to write to this class.
+   * Even if this returns false, the user may still be able to write it if
+   * getPublicWriteAccess returns true or a role that the user belongs to has
+   * write access.
+   *
+   * @param userId An instance of Parse.User or its objectId, or a Parse.Role.
+   * @returns {boolean}
+   */
+  getWriteAccess(userId: Entity): boolean {
+    return (
+      this._getAccess('create', userId) &&
+      this._getAccess('update', userId) &&
+      this._getAccess('delete', userId) &&
+      this._getAccess('addField', userId)
+    );
+  }
+
+  /**
+   * Sets whether the public is allowed to read from this class.
+   *
+   * @param {boolean} allowed
+   */
+  setPublicReadAccess(allowed: boolean) {
+    this.setReadAccess(PUBLIC_KEY, allowed);
+  }
+
+  /**
+   * Gets whether the public is allowed to read from this class.
+   *
+   * @returns {boolean}
+   */
+  getPublicReadAccess(): boolean {
+    return this.getReadAccess(PUBLIC_KEY);
+  }
+
+  /**
+   * Sets whether the public is allowed to write to this class.
+   *
+   * @param {boolean} allowed
+   */
+  setPublicWriteAccess(allowed: boolean) {
+    this.setWriteAccess(PUBLIC_KEY, allowed);
+  }
+
+  /**
+   * Gets whether the public is allowed to write to this class.
+   *
+   * @returns {boolean}
+   */
+  getPublicWriteAccess(): boolean {
+    return this.getWriteAccess(PUBLIC_KEY);
+  }
+
+  /**
+   * Sets whether the public is allowed to protect fields in this class.
+   *
+   * @param {string[]} fields
+   */
+  setPublicProtectedFields(fields: string[]) {
+    this.setProtectedFields(PUBLIC_KEY, fields);
+  }
+
+  /**
+   * Gets whether the public is allowed to read fields from this class.
+   *
+   * @returns {string[]}
+   */
+  getPublicProtectedFields(): string[] {
+    return this.getProtectedFields(PUBLIC_KEY);
+  }
+
+  /**
+   * Gets whether users belonging to the given role are allowed
+   * to read from this class. Even if this returns false, the role may
+   * still be able to write it if a parent role has read access.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @returns {boolean} true if the role has read access. false otherwise.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  getRoleReadAccess(role: ParseRole | string): boolean {
+    return this.getReadAccess(this._getRoleName(role));
+  }
+
+  /**
+   * Gets whether users belonging to the given role are allowed
+   * to write to this user. Even if this returns false, the role may
+   * still be able to write it if a parent role has write access.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @returns {boolean} true if the role has write access. false otherwise.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  getRoleWriteAccess(role: ParseRole | string): boolean {
+    return this.getWriteAccess(this._getRoleName(role));
+  }
+
+  /**
+   * Sets whether users belonging to the given role are allowed
+   * to read from this class.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @param {boolean} allowed Whether the given role can read this object.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  setRoleReadAccess(role: ParseRole | string, allowed: boolean) {
+    this.setReadAccess(this._getRoleName(role), allowed);
+  }
+
+  /**
+   * Sets whether users belonging to the given role are allowed
+   * to write to this class.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @param {boolean} allowed Whether the given role can write this object.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  setRoleWriteAccess(role: ParseRole | string, allowed: boolean) {
+    this.setWriteAccess(this._getRoleName(role), allowed);
+  }
+
+  /**
+   * Gets whether users belonging to the given role are allowed
+   * to count to this user. Even if this returns false, the role may
+   * still be able to count it if a parent role has count access.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @returns {string[]}
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  getRoleProtectedFields(role: ParseRole | string): string[] {
+    return this.getProtectedFields(this._getRoleName(role));
+  }
+
+  /**
+   * Sets whether users belonging to the given role are allowed
+   * to set access field in this class.
+   *
+   * @param role The name of the role, or a Parse.Role object.
+   * @param {string[]} fields Fields to be protected by Role.
+   * @throws {TypeError} If role is neither a Parse.Role nor a String.
+   */
+  setRoleProtectedFields(role: ParseRole | string, fields: string[]) {
+    this.setProtectedFields(this._getRoleName(role), fields);
+  }
+}
+
+export default ParseCLP;
diff --git a/src/ParseSchema.js b/src/ParseSchema.js
index 6d770e9..c7fa844 100644
--- a/src/ParseSchema.js
+++ b/src/ParseSchema.js
@@ -11,6 +11,9 @@
 
 import CoreManager from './CoreManager';
 import ParseObject from './ParseObject';
+import ParseCLP from './ParseCLP';
+
+import type { PermissionsMap } from './ParseCLP';
 
 const FIELD_TYPES = [
   'String',
@@ -190,11 +193,15 @@ class ParseSchema {
    * Sets Class Level Permissions when creating / updating a Schema.
    * EXERCISE CAUTION, running this may override CLP for this schema and cannot be reversed
    *
-   * @param {object} clp Class Level Permissions
+   * @param {object | Parse.CLP} clp Class Level Permissions
    * @returns {Parse.Schema} Returns the schema, so you can chain this call.
    */
-  setCLP(clp: { [key: string]: mixed }) {
-    this._clp = clp;
+  setCLP(clp: PermissionsMap | ParseCLP) {
+    if (clp instanceof ParseCLP) {
+      this._clp = clp.toJSON();
+    } else {
+      this._clp = clp;
+    }
     return this;
   }
 


Test patch:
diff --git a/integration/test/ParseLiveQueryTest.js b/integration/test/ParseLiveQueryTest.js
index 89e33a0..ffff28c 100644
--- a/integration/test/ParseLiveQueryTest.js
+++ b/integration/test/ParseLiveQueryTest.js
@@ -58,6 +58,32 @@ describe('Parse LiveQuery', () => {
     await object.save();
   });
 
+  it('can subscribe to query with null connect fields', async done => {
+    const client = new Parse.LiveQueryClient({
+      applicationId: 'integration',
+      serverURL: 'ws://localhost:1337',
+      javascriptKey: null,
+      masterKey: null,
+      sessionToken: null,
+      installationId: null,
+    });
+    client.open();
+    const object = new TestObject();
+    await object.save();
+
+    const query = new Parse.Query(TestObject);
+    query.equalTo('objectId', object.id);
+    const subscription = await client.subscribe(query);
+    subscription.on('update', async object => {
+      assert.equal(object.get('foo'), 'bar');
+      client.close();
+      done();
+    });
+    await subscription.subscribePromise;
+    object.set({ foo: 'bar' });
+    await object.save();
+  });
+
   it('can subscribe to multiple queries', async () => {
     const objectA = new TestObject();
     const objectB = new TestObject();
diff --git a/integration/test/ParseSchemaTest.js b/integration/test/ParseSchemaTest.js
index dfa48a2..9837332 100644
--- a/integration/test/ParseSchemaTest.js
+++ b/integration/test/ParseSchemaTest.js
@@ -264,7 +264,7 @@ describe('Schema', () => {
     assert.deepEqual(json, expected);
   });
 
-  it('save class level permissions', async () => {
+  it('save class level permissions json', async () => {
     const clp = {
       get: { requiresAuthentication: true },
       find: {},
@@ -281,6 +281,25 @@ describe('Schema', () => {
     assert.deepEqual(schema.classLevelPermissions, clp);
   });
 
+  it('save class level permissions object', async () => {
+    const permissionsMap = {
+      get: { '*': true },
+      find: {},
+      count: {},
+      create: { '*': true },
+      update: { '*': true },
+      delete: {},
+      addField: {},
+      protectedFields: {},
+    };
+    const clp = new Parse.CLP(permissionsMap);
+    const testSchema = new Parse.Schema('SchemaTest');
+    testSchema.setCLP(clp);
+    const schema = await testSchema.save();
+    assert.deepEqual(schema.classLevelPermissions, permissionsMap);
+    assert.deepEqual(schema.classLevelPermissions, clp.toJSON());
+  });
+
   it('update class level permissions', async () => {
     const clp = {
       get: { requiresAuthentication: true },
diff --git a/src/__tests__/ParseCLP-test.js b/src/__tests__/ParseCLP-test.js
new file mode 100644
index 0000000..44a28eb
--- /dev/null
+++ b/src/__tests__/ParseCLP-test.js
@@ -0,0 +1,680 @@
+/**
+ * Copyright (c) 2015-present, Parse, LLC.
+ * All rights reserved.
+ *
+ * This source code is licensed under the BSD-style license found in the
+ * LICENSE file in the root directory of this source tree. An additional grant
+ * of patent rights can be found in the PATENTS file in the same directory.
+ */
+
+jest.dontMock('../ParseCLP');
+
+const mockRole = function (name) {
+  this.name = name;
+};
+
+mockRole.prototype.getName = function () {
+  return this.name;
+};
+
+jest.setMock('../ParseRole', mockRole);
+
+const ParseCLP = require('../ParseCLP').default;
+const ParseUser = require('../ParseUser').default;
+const ParseRole = require('../ParseRole');
+
+function generateReadCLP(key) {
+  return {
+    get: { [key]: true },
+    find: { [key]: true },
+    count: { [key]: true },
+    create: {},
+    update: {},
+    delete: {},
+    addField: {},
+    protectedFields: {},
+  };
+}
+
+function generateWriteCLP(key) {
+  return {
+    get: {},
+    find: {},
+    count: {},
+    create: { [key]: true },
+    update: { [key]: true },
+    delete: { [key]: true },
+    addField: { [key]: true },
+    protectedFields: {},
+  };
+}
+
+const DEFAULT_CLP = {
+  get: {},
+  find: {},
+  count: {},
+  create: {},
+  update: {},
+  delete: {},
+  addField: {},
+  protectedFields: {},
+};
+
+const READ_CLP = generateReadCLP('uid');
+const WRITE_CLP = generateWriteCLP('uid');
+const PUBLIC_READ_CLP = generateReadCLP('*');
+const PUBLIC_WRITE_CLP = generateWriteCLP('*');
+const ROLE_READ_CLP = generateReadCLP('role:admin');
+const ROLE_WRITE_CLP = generateWriteCLP('role:admin');
+
+describe('ParseCLP', () => {
+  it('can be constructed with no arguments', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+  });
+
+  it('can be constructed with a ParseUser', () => {
+    const u = new ParseUser();
+    u.id = 'uid';
+    const a = new ParseCLP(u);
+    expect(a.permissionsMap).toEqual({
+      get: { uid: true },
+      find: { uid: true },
+      count: { uid: true },
+      create: { uid: true },
+      update: { uid: true },
+      delete: { uid: true },
+      addField: { uid: true },
+      protectedFields: {},
+    });
+  });
+
+  it('can be constructed with a map of user IDs', () => {
+    const userCLP = {
+      get: { uid: true },
+      find: { uid: true },
+      count: { uid: true },
+      create: { uid: true },
+      update: { uid: true },
+      delete: { uid: true },
+      addField: { uid: true },
+      protectedFields: {},
+    };
+    const a = new ParseCLP(userCLP);
+    expect(a.permissionsMap).toEqual(userCLP);
+  });
+
+  it('throws when constructed with an invalid permissions map', () => {
+    let err = function () {
+      new ParseCLP({ foobar: { foo: true, bar: false } });
+    };
+    expect(err).toThrow('Tried to create an CLP with an invalid permission type.');
+    err = function () {
+      new ParseCLP({ get: { uid: 12 } });
+    };
+    expect(err).toThrow('Tried to create an CLP with an invalid permission value.');
+  });
+
+  it('throws a helpful error when constructed with a function', () => {
+    expect(function () {
+      new ParseCLP(function () {});
+    }).toThrow('ParseCLP constructed with a function. Did you forget ()?');
+  });
+
+  it('throws when setting an invalid input', () => {
+    const a = new ParseCLP();
+    expect(a.setReadAccess.bind(a, 12, true)).toThrow('userId must be a string.');
+  });
+
+  it('throws when getting an invalid user id', () => {
+    const u = new ParseUser();
+    const a = new ParseCLP();
+    expect(a.getReadAccess.bind(a, u)).toThrow('Cannot get access for a Parse.User without an id.');
+  });
+
+  it('throws when setting an invalid access', () => {
+    const a = new ParseCLP();
+    expect(a.setReadAccess.bind(a, 'uid', 12)).toThrow('allowed must be either true or false.');
+  });
+
+  it('throws when setting an invalid role', () => {
+    const a = new ParseCLP();
+    expect(a.setRoleReadAccess.bind(a, 12, true)).toThrow('role must be a Parse.Role or a String');
+    expect(a.setRoleWriteAccess.bind(a, 12, true)).toThrow('role must be a Parse.Role or a String');
+  });
+
+  it('throws when setting an invalid protectedFields', () => {
+    const a = new ParseCLP();
+    const u = new ParseUser();
+    const r = new ParseRole();
+    expect(a.setProtectedFields.bind(a, 12)).toThrow('userId must be a string.');
+    expect(a.setProtectedFields.bind(a, u)).toThrow(
+      'Cannot get access for a Parse.User without an id.'
+    );
+    expect(a.setProtectedFields.bind(a, r)).toThrow('role must be a Parse.Role or a String');
+    expect(a.setProtectedFields.bind(a, new ParseRole('admin'), 'not_field_array')).toThrow(
+      'fields must be an array of strings or undefined.'
+    );
+  });
+
+  it('throws when setting an invalid group pointer permissions', () => {
+    const a = new ParseCLP();
+    expect(a.setReadUserFields.bind(a, 12)).toThrow(
+      'readUserFields.pointerFields must be an array of strings or undefined.'
+    );
+    expect(a.setWriteUserFields.bind(a, 12)).toThrow(
+      'writeUserFields.pointerFields must be an array of strings or undefined.'
+    );
+
+    const clp = {
+      get: {},
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+      readUserFields: [1234],
+      writeUserFields: ['owner'],
+    };
+    expect(function () {
+      new ParseCLP(clp);
+    }).toThrow('Tried to create an CLP with an invalid permission value.');
+  });
+
+  it('can be rendered to JSON format', () => {
+    const permissionsMap = {
+      get: { uid: true },
+      find: { uid: true },
+      count: { uid: true },
+      create: { admin: true },
+      update: { admin: true },
+      delete: { admin: true },
+      addField: { admin: true },
+      protectedFields: { admin: ['objectId', 'field1'] },
+    };
+    const a = new ParseCLP(permissionsMap);
+    expect(a.toJSON()).toEqual(permissionsMap);
+  });
+
+  it('can set protectedFields', () => {
+    const a = new ParseCLP();
+    const r = new ParseRole('admin');
+
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+
+    a.setProtectedFields('uid', ['admin']);
+    a.setPublicProtectedFields(['foo']);
+    a.setRoleProtectedFields(r, ['bar']);
+
+    expect(a.toJSON()).toEqual({
+      get: {},
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {
+        uid: ['admin'],
+        '*': ['foo'],
+        'role:admin': ['bar'],
+      },
+    });
+    expect(a.getProtectedFields('uid')).toEqual(['admin']);
+    expect(a.getPublicProtectedFields()).toEqual(['foo']);
+    expect(a.getRoleProtectedFields(r)).toEqual(['bar']);
+
+    a.setProtectedFields('uid', undefined);
+    a.setPublicProtectedFields(undefined);
+    a.setRoleProtectedFields(r, undefined);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can get and set requiresAuthentication', () => {
+    const a = new ParseCLP();
+    a.setGetRequiresAuthentication(true);
+    expect(a.toJSON()).toEqual({
+      get: { requiresAuthentication: true },
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+    });
+    expect(a.getGetRequiresAuthentication()).toBe(true);
+    a.setGetRequiresAuthentication(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can get and set pointerFields', () => {
+    const testCLP = {
+      get: { pointerFields: ['owner'] },
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+    };
+    const pointerCLP = new ParseCLP(testCLP);
+    expect(pointerCLP.toJSON()).toEqual(testCLP);
+
+    const a = new ParseCLP();
+    a.setGetPointerFields(['owner']);
+    expect(a.toJSON()).toEqual(testCLP);
+    expect(a.getGetPointerFields()).toEqual(['owner']);
+
+    a.setGetPointerFields(undefined);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getGetPointerFields()).toEqual(undefined);
+  });
+
+  it('can get and set group pointer permissions', () => {
+    const testCLP = {
+      get: {},
+      find: {},
+      count: {},
+      create: {},
+      update: {},
+      delete: {},
+      addField: {},
+      protectedFields: {},
+      readUserFields: ['parent'],
+      writeUserFields: ['owner'],
+    };
+    const groupCLP = new ParseCLP(testCLP);
+    expect(groupCLP.toJSON()).toEqual(testCLP);
+
+    const a = new ParseCLP();
+    a.setWriteUserFields(['owner']);
+    a.setReadUserFields(['parent']);
+    expect(a.toJSON()).toEqual(testCLP);
+    expect(a.getWriteUserFields()).toEqual(['owner']);
+    expect(a.getReadUserFields()).toEqual(['parent']);
+
+    a.setWriteUserFields(undefined);
+    a.setReadUserFields(undefined);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can set read access', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+
+    a.setReadAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess('uid')).toBe(false);
+
+    a.setReadAccess('uid', true);
+    expect(a.permissionsMap).toEqual(READ_CLP);
+    expect(a.getReadAccess('uid')).toBe(true);
+
+    a.setReadAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess('uid')).toBe(false);
+
+    const u = new ParseUser();
+    u.id = 'uid';
+
+    a.setReadAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess(u)).toBe(false);
+
+    a.setReadAccess(u, true);
+    expect(a.permissionsMap).toEqual(READ_CLP);
+    expect(a.getReadAccess(u)).toBe(true);
+
+    a.setReadAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getReadAccess(u)).toBe(false);
+  });
+
+  it('can get read access', () => {
+    const a = new ParseCLP();
+    a.setGetAccess('uid', true);
+    a.setFindAccess('uid', true);
+    a.setCountAccess('uid', true);
+    expect(a.toJSON()).toEqual(READ_CLP);
+    expect(a.getReadAccess('uid')).toBe(true);
+    expect(a.getGetAccess('uid')).toBe(true);
+    expect(a.getFindAccess('uid')).toBe(true);
+    expect(a.getCountAccess('uid')).toBe(true);
+    a.setGetAccess('uid', false);
+    a.setFindAccess('uid', false);
+    a.setCountAccess('uid', false);
+    expect(a.getReadAccess('uid')).toBe(false);
+    expect(a.getGetAccess('uid')).toBe(false);
+    expect(a.getFindAccess('uid')).toBe(false);
+    expect(a.getCountAccess('uid')).toBe(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can set write access', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+
+    a.setWriteAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess('uid')).toBe(false);
+
+    a.setWriteAccess('uid', true);
+    expect(a.permissionsMap).toEqual(WRITE_CLP);
+    expect(a.getWriteAccess('uid')).toBe(true);
+
+    a.setWriteAccess('uid', false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess('uid')).toBe(false);
+
+    const u = new ParseUser();
+    u.id = 'uid';
+
+    a.setWriteAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess(u)).toBe(false);
+
+    a.setWriteAccess(u, true);
+    expect(a.permissionsMap).toEqual(WRITE_CLP);
+    expect(a.getWriteAccess(u)).toBe(true);
+
+    a.setWriteAccess(u, false);
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getWriteAccess(u)).toBe(false);
+  });
+
+  it('can get write access', () => {
+    const a = new ParseCLP();
+    a.setCreateAccess('uid', true);
+    a.setDeleteAccess('uid', true);
+    a.setUpdateAccess('uid', true);
+    a.setAddFieldAccess('uid', true);
+    expect(a.toJSON()).toEqual(WRITE_CLP);
+    expect(a.getWriteAccess('uid')).toBe(true);
+    expect(a.getCreateAccess('uid')).toBe(true);
+    expect(a.getDeleteAccess('uid')).toBe(true);
+    expect(a.getUpdateAccess('uid')).toBe(true);
+    expect(a.getAddFieldAccess('uid')).toBe(true);
+    a.setCreateAccess('uid', false);
+    a.setDeleteAccess('uid', false);
+    a.setUpdateAccess('uid', false);
+    a.setAddFieldAccess('uid', false);
+    expect(a.getWriteAccess('uid')).toBe(false);
+    expect(a.getCreateAccess('uid')).toBe(false);
+    expect(a.getDeleteAccess('uid')).toBe(false);
+    expect(a.getUpdateAccess('uid')).toBe(false);
+    expect(a.getAddFieldAccess('uid')).toBe(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+  });
+
+  it('can handle public access', () => {
+    const a = new ParseCLP();
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getPublicReadAccess()).toBe(false);
+    expect(a.getPublicWriteAccess()).toBe(false);
+
+    a.setPublicGetAccess(true);
+    a.setPublicFindAccess(true);
+    a.setPublicCountAccess(true);
+    expect(a.toJSON()).toEqual(PUBLIC_READ_CLP);
+    expect(a.getPublicReadAccess()).toBe(true);
+    expect(a.getPublicGetAccess()).toBe(true);
+    expect(a.getPublicFindAccess()).toBe(true);
+    expect(a.getPublicCountAccess()).toBe(true);
+
+    a.setPublicReadAccess(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getPublicReadAccess()).toBe(false);
+    expect(a.getPublicGetAccess()).toBe(false);
+    expect(a.getPublicFindAccess()).toBe(false);
+    expect(a.getPublicCountAccess()).toBe(false);
+
+    a.setPublicCreateAccess(true);
+    a.setPublicDeleteAccess(true);
+    a.setPublicUpdateAccess(true);
+    a.setPublicAddFieldAccess(true);
+    expect(a.toJSON()).toEqual(PUBLIC_WRITE_CLP);
+    expect(a.getPublicWriteAccess()).toBe(true);
+    expect(a.getPublicCreateAccess()).toBe(true);
+    expect(a.getPublicDeleteAccess()).toBe(true);
+    expect(a.getPublicUpdateAccess()).toBe(true);
+    expect(a.getPublicAddFieldAccess()).toBe(true);
+
+    a.setPublicWriteAccess(false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getPublicWriteAccess()).toBe(false);
+    expect(a.getPublicCreateAccess()).toBe(false);
+    expect(a.getPublicDeleteAccess()).toBe(false);
+    expect(a.getPublicUpdateAccess()).toBe(false);
+    expect(a.getPublicAddFieldAccess()).toBe(false);
+
+    const w = new ParseCLP();
+    w.setPublicWriteAccess(true);
+    expect(w.toJSON()).toEqual(PUBLIC_WRITE_CLP);
+
+    const r = new ParseCLP();
+    r.setPublicReadAccess(true);
+    expect(r.toJSON()).toEqual(PUBLIC_READ_CLP);
+  });
+
+  it('can get clp with role', () => {
+    const role = new ParseRole('admin');
+    const a = new ParseCLP(role);
+
+    expect(a.toJSON()).toEqual({
+      get: { 'role:admin': true },
+      find: { 'role:admin': true },
+      count: { 'role:admin': true },
+      create: { 'role:admin': true },
+      update: { 'role:admin': true },
+      delete: { 'role:admin': true },
+      addField: { 'role:admin': true },
+      protectedFields: {},
+    });
+    expect(a.getRoleReadAccess('admin')).toBe(true);
+    expect(a.getRoleReadAccess(role)).toBe(true);
+    expect(a.getReadAccess(role)).toBe(true);
+  });
+
+  it('can get role read access', () => {
+    const a = new ParseCLP({
+      get: { 'role:admin': true },
+      find: { 'role:admin': true },
+      count: { 'role:admin': true },
+      create: { 'role:admin': true },
+      update: { 'role:admin': true },
+      delete: { 'role:admin': true },
+      addField: { 'role:admin': true },
+      protectedFields: { 'role:admin': [] },
+    });
+
+    expect(a.getRoleReadAccess('admin')).toBe(true);
+    expect(a.getRoleReadAccess(new ParseRole('admin'))).toBe(true);
+    expect(a.getReadAccess(new ParseRole('admin'))).toBe(true);
+  });
+
+  it('can get role write access', () => {
+    const a = new ParseCLP({
+      get: { 'role:admin': true },
+      find: { 'role:admin': true },
+      count: { 'role:admin': true },
+      create: { 'role:admin': true },
+      update: { 'role:admin': true },
+      delete: { 'role:admin': true },
+      addField: { 'role:admin': true },
+      protectedFields: { 'role:admin': [] },
+    });
+
+    expect(a.getRoleWriteAccess('admin')).toBe(true);
+    expect(a.getRoleWriteAccess(new ParseRole('admin'))).toBe(true);
+    expect(a.getWriteAccess(new ParseRole('admin'))).toBe(true);
+  });
+
+  it('can set role read access', () => {
+    const a = new ParseCLP();
+    const adminRole = new ParseRole('admin');
+
+    expect(a.getRoleReadAccess('admin')).toBe(false);
+    expect(a.getRoleReadAccess(adminRole)).toBe(false);
+
+    a.setRoleReadAccess('admin', true);
+    expect(a.getRoleReadAccess('admin')).toBe(true);
+    expect(a.getRoleWriteAccess('admin')).toBe(false);
+
+    a.setRoleReadAccess(adminRole, false);
+    expect(a.getRoleReadAccess(adminRole)).toBe(false);
+
+    a.setReadAccess(adminRole, true);
+    expect(a.getReadAccess(adminRole)).toBe(true);
+  });
+
+  it('can set role write access', () => {
+    const a = new ParseCLP();
+
+    expect(a.getRoleWriteAccess('admin')).toBe(false);
+    expect(a.getRoleWriteAccess(new ParseRole('admin'))).toBe(false);
+
+    a.setRoleWriteAccess('admin', true);
+    expect(a.getRoleWriteAccess('admin')).toBe(true);
+    expect(a.getRoleReadAccess('admin')).toBe(false);
+
+    a.setRoleWriteAccess(new ParseRole('admin'), false);
+    expect(a.getRoleWriteAccess(new ParseRole('admin'))).toBe(false);
+
+    a.setWriteAccess(new ParseRole('admin'), true);
+    expect(a.getWriteAccess(new ParseRole('admin'))).toBe(true);
+  });
+
+  it('can handle role access', () => {
+    const a = new ParseCLP();
+    const r = new ParseRole('admin');
+
+    expect(a.permissionsMap).toEqual(DEFAULT_CLP);
+    expect(a.getRoleReadAccess(r)).toBe(false);
+    expect(a.getRoleWriteAccess(r)).toBe(false);
+
+    a.setRoleGetAccess(r, true);
+    a.setRoleFindAccess(r, true);
+    a.setRoleCountAccess(r, true);
+    expect(a.toJSON()).toEqual(ROLE_READ_CLP);
+    expect(a.getRoleReadAccess(r)).toBe(true);
+    expect(a.getRoleGetAccess(r)).toBe(true);
+    expect(a.getRoleFindAccess(r)).toBe(true);
+    expect(a.getRoleCountAccess(r)).toBe(true);
+
+    a.setRoleReadAccess(r, false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getRoleReadAccess(r)).toBe(false);
+    expect(a.getRoleGetAccess(r)).toBe(false);
+    expect(a.getRoleFindAccess(r)).toBe(false);
+    expect(a.getRoleCountAccess(r)).toBe(false);
+
+    a.setRoleCreateAccess(r, true);
+    a.setRoleDeleteAccess(r, true);
+    a.setRoleUpdateAccess(r, true);
+    a.setRoleAddFieldAccess(r, true);
+    expect(a.toJSON()).toEqual(ROLE_WRITE_CLP);
+    expect(a.getRoleWriteAccess(r)).toBe(true);
+    expect(a.getRoleCreateAccess(r)).toBe(true);
+    expect(a.getRoleDeleteAccess(r)).toBe(true);
+    expect(a.getRoleUpdateAccess(r)).toBe(true);
+    expect(a.getRoleAddFieldAccess(r)).toBe(true);
+
+    a.setRoleWriteAccess(r, false);
+    expect(a.toJSON()).toEqual(DEFAULT_CLP);
+    expect(a.getRoleWriteAccess(r)).toBe(false);
+    expect(a.getRoleCreateAccess(r)).toBe(false);
+    expect(a.getRoleDeleteAccess(r)).toBe(false);
+    expect(a.getRoleUpdateAccess(r)).toBe(false);
+    expect(a.getRoleAddFieldAccess(r)).toBe(false);
+
+    const b = new ParseCLP();
+    b.setRoleWriteAccess(r, true);
+    expect(b.toJSON()).toEqual(ROLE_WRITE_CLP);
+
+    const c = new ParseCLP();
+    c.setRoleReadAccess(r, true);
+    expect(c.toJSON()).toEqual(ROLE_READ_CLP);
+  });
+
+  it('can test equality against another CLP', () => {
+    const a = new ParseCLP();
+    const b = new ParseCLP();
+
+    expect(a.equals(a)).toBe(true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setPublicReadAccess(true);
+    expect(a.equals(a)).toBe(true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setPublicWriteAccess(true);
+    expect(b.equals(b)).toBe(true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    a.setPublicWriteAccess(true);
+    b.setPublicReadAccess(true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setPublicReadAccess(false);
+    a.setPublicWriteAccess(false);
+    b.setPublicReadAccess(false);
+    b.setPublicWriteAccess(false);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+    expect(a.equals(new ParseCLP())).toBe(true);
+
+    a.setReadAccess('uid', true);
+    b.setReadAccess('uid', true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setWriteAccess('uid', true);
+    b.setWriteAccess('uid', true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setWriteAccess('uid2', true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setWriteAccess('uid2', true);
+    expect(a.equals(b)).toBe(true);
+    expect(b.equals(a)).toBe(true);
+
+    a.setPublicReadAccess(true);
+    a.setReadAccess('uid', true);
+    b.setReadAccess('uid', true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setReadAccess('uid2', true);
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    b.setReadAccess('uid2', false);
+    b.setPublicReadAccess(true);
+    b.permissionsMap.get['*'] = 'HACKED';
+    expect(a.equals(b)).toBe(false);
+    expect(b.equals(a)).toBe(false);
+
+    const r = new ParseRole('admin');
+    expect(a.equals(r)).toBe(false);
+
+    const hacked = new ParseCLP();
+    delete hacked.permissionsMap.get;
+    expect(a.equals(hacked)).toBe(false);
+    expect(hacked.equals(a)).toBe(false);
+
+    hacked.permissionsMap.newPermission = {};
+    expect(a.equals(hacked)).toBe(false);
+    expect(hacked.equals(a)).toBe(false);
+  });
+});
diff --git a/src/__tests__/ParseSchema-test.js b/src/__tests__/ParseSchema-test.js
index f1ad11f..92386ee 100644
--- a/src/__tests__/ParseSchema-test.js
+++ b/src/__tests__/ParseSchema-test.js
@@ -22,7 +22,16 @@ const mockObject = function (className, id) {
 };
 jest.setMock('../ParseObject', mockObject);
 
+const mockCLP = function (clp) {
+  this.permissionsMap = clp;
+  this.toJSON = function () {
+    return { ...this.permissionsMap };
+  };
+};
+jest.setMock('../ParseCLP', mockCLP);
+
 const ParseObject = require('../ParseObject');
+const ParseCLP = require('../ParseCLP');
 const ParseSchema = require('../ParseSchema').default;
 const CoreManager = require('../CoreManager');
 
@@ -148,6 +157,10 @@ describe('ParseSchema', () => {
     };
     schema.setCLP(clp);
     expect(schema._clp).toEqual(clp);
+
+    const clpObj = new ParseCLP(clp);
+    schema.setCLP(clpObj);
+    expect(schema._clp).toEqual(clpObj.toJSON());
     done();
   });