Advanced Guides

More power to your App with Parse Server 3.1

Introduction

The Parse Community recently released version 3.1 of the Parse Server. This update has cleaned up Cloud Code syntax: it is far more amenable to leveraging the es6 async and await constructs.

Additionally, some idiosyncrasies associated with using Parse were dropped, for example, Cloud functions simply return a Promise rather than using the error or success messages on the Response object.

You can upgrade your apps on Back4App easily on your Dashboard. This guide will demonstrate how to upgrade your code to leverage the new features of 3.1.

To follow this guide you are welcome to take a look at the example project provided.

This is a guest tutorial written by John Considine, lead developer at K-Optional.

Goals

  • To update your Back4App Parse Server to 3.1 and migrate your Cloud Code accordingly.

Prerequisites

To complete this tutorial, you need:

  • An existing Back4App application that’s using Parse Server 2.x

Summary of changes

The most notable changes are the following:

1. Cloud Code runs with Parse-SDK 2.x.

Previously, Cloud Code ran with Parse-SDK 1.x. With Parse Server 3.1, it runs Parse-SDK 2.x.

Look at Parse SDK releases to understand better what this entails. This major version bump mostly involves bug fixes. It also adds containedBy and includeAll query methods, as well as abilities to fetch an object with includes.

2. Aggregate update

Since Parse 2.7.1, you can use the aggregate method on a query. This allows you to leverage the underlying MongoDB a little bit more. Now, the syntax for the aggregate method on Parse.Query has been updated.

You can execute a query using two stages: the match and the group stage.

1
2
3
4
5
6
// matches users who's name are Foo, and groups by their objectId
const pipeline = {
  pipeline: [{ group: { objectId: {} } }, { match: { name: 'Foo' } }]
};
var query = new Parse.Query("Person");
query.aggregate(pipeline)

Previously, you didn’t need the pipeline key in the pipeline object. Due to the underlying API, you must now explicitely include the pipeline key. The value should be an array of one or two stages, featuring group and match.

Look at Parse Official Documentation for more specific examples.

3. Under-the-hood optimizations

Some under-the-hood optimizations has been made. For example, a Parse LiveQuery will fetch Class Level Permissions (CLPs) along with data to prevent double database access.

4. Parse Reset Email

When requesting a password reset email, the server will return success even if that email is not saved. Additionally, password reset tokens expire when a user’s email is reset.

5. Cloud triggers update

With this release you can share data between the beforeSave and afterSave triggers on the same object. For example:

1
2
3
4
5
6
7
8
9
Parse.Cloud.beforeSave('Comment', async request => {
  request.context = {
    foo: 'bar'
  };
});

Parse.Cloud.afterSave('Comment', async request => {
  console.log(request.context.foo); //Bar
});

You can see more about changes on Parse Server in the official Parse 3.1 Changelog by clicking here.

6. LiveQuery Improvement

The Parse LiveQuery client allows you to subscribe to queries, and receive updates from the server as they come in. Traditional Queries are executed once from the client, so this is very helpful for cases like messaging etc.

With Back4App you can also take advantage of this technology.

With the release of 3.x, the Parse community has improved the system for LiveQuery ACLs.

You can pass a session token now to the subscribe method of a live query, and the Parse Server will manage only returning results that this user has access to. For example, a user may have read / write access to certain ‘Messages’, but not all.

1
2
3
4
let query = new Parse.Query('Message');
// you can get session token with
// Parse.User.current().getSessionToken() when logged in
let subscription = client.subscribe(query, sessionToken); 

The above code will automatically subscribe to all messages that the user has access to, relieving you from the responsibility of querying specific messages.

The main part of the changes pertains to how Cloud Code is handled. For this, see the migration guide below.

Step 1 - Aligning technical fundamentals

We’ll start with an example cloud project using the 2.x release. That way we can navigate through the appropriate changes of syntax. You can see the repository for this example project.

A note on async / await

If you’re familiar with async and await, you can skip this section.

The evolution of asynchronous code in Javascript looks something like this:

Any modern Javascript application will likely use all three. Callback functions involve passing a function as an argument to another function. This second function can execute the first at some point.

1
2
3
4
5
// Callbacks:
// executes callback function after waiting 100 milliseconds
setTimeout(function() {
  alert('My callback function');
}, 100);

Callbacks are essential, but can be unwieldy when it comes to chaining many of them. Specifically, nesting several layers can be difficult to read, and error handling proves difficult. Hence in ES2015 the Promise was introduced.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// promises
// executes several promises in a row with no significant nesting
const myPromise = new Promise(function(resolve, reject) {
  setTimeout(function() {
    if (Math.random() < 0.2) reject(new Error('Random failure!'));
    resolve('Finished');
  }, 100);
});

// Executes this promise 4 times and catches errors involved
myPromise
  .then(() => myPromise)
  .then(() => myPromise)
  .then(() => myPromise)
  .then(() => myPromise)
  .catch(e => console.log(e));

Promises improve the readability of asynchronous programming. They also make pipelines more explicit. But even bigger strides were made in ES2017 with the async / await constructs. Your code can now wait for the results of a Promise without relying on the then / catch blocks (which can also become tough to read).

1
2
3
4
5
6
7
8
9
10
// Using the definition of myPromise from the above code:
async function foo() {
  try {
    let result = await myPromise;
    result = await myPromise;
    result = await myPromise;
  } catch (e) {
    console.log(e);
  }
}

Perhaps for a very simple example, this may not seem more elegant than plain promises. But awaiting the results of an asynchronous function is often precisely what we want to do. Hence, it truly optimizes the readability of our code.

Support for async / await

As aforementioned, async / await were included in the ECMAscript 2017 specification (es8). For server code, versioning is hardly an issue since you can update to the version of Node.js that supports these features. Rest assured, Back4App’s environment obviously supports recent stable versions. For browser code, transpilers like Babel will produce a es2016 compatible from code that uses async / await and works in modern browsers.

Step 2: Thinking about your code differently

The main change with Cloud Code involves what the developer does versus what the library does. Previously, you would explicitly manage the response. Since most Cloud Code will execute asynchronously - making database queries and writes - it makes more sense to return a Promise, reducing the boilerplate code.

The intuition behind Cloud functions is that there is minimal setup and configuration involved with writing server-side code. This release embodies that idea; keep this in mind as you’re refactoring and creating new functions.

To show how Cloud Code functions work in Parse Server 3.1, we rewrote a functional Cloud Code sample from a version of Parse Server before migration. You can find this code clicking here. The same Cloud Code function is written in Parse 3.1 like shown below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// Cloud Code BEFORE migration
// Full code found in link above
const POST = 'Post';
Parse.Cloud.define('posts:get', function(request, response) {
  // needs a post ID
  return new Parse.Query(POST)
    .get(request.params.id, { useMasterKey: true })
    .then(post => {
      response.success(post);
    })
    .catch(e => {
      response.error({ message: e.message });
    });
});

Step 3: Adding all async markers

Any function that uses await must be declared with the async modifier. This simple refactoring will attach async to all Cloud Functions. It will also replace them with arrow functions as they are more succinct in this case (and what the updated official Parse guide uses).

1
2
3
4
5
6
7
8
9
10
// Snippet of step 2 code refactoring. See full code
//  here in the link at the top of this step
const POST = 'Post';
const COMMENT = 'Comment';

Parse.Cloud.define('posts:get', async (request) => {
  // Needs a post ID
  return new Parse.Query(POST)
    .get(request.params.id, { useMasterKey: true });
}    

Your code will look like this after this refactoring

Nothing crazy so far. In the next step we’ll get our money’s worth for this change.

Step 4: Removing references to response, employ await

Your code will look like this after this refactoring

This step is a little bit trickier. We need to:

  • Remove all references to the ‘response’ variable, returning a promise instead
  • In the case of multiple query / save functions, await the response

Checkout the comment create method to see how this is done

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
// Snippet of step 3 code refactoring. See full code
//  here in the link at the top of this step
Parse.Cloud.define('comment:create', async request => {
  // Post should have text and should have a user and a post id
  if (!request.user) {
    throw new Error('unauthenticated!');
  }

  if (!request.params.text) {
    throw new Error('A comment needs text!');
  }
  if (!request.params.post_id) {
    throw new Error('A comment needs a post!');
  }

  //   Get the post

  const post = await new Parse.Query(POST).get(request.params.post_id, {
    useMasterKey: true
  });
  return new Parse.Object(COMMENT, {
    text: request.params.text,
    user: request.user,
    post: post
  }).save(null, { useMasterKey: true });
});

Note that:

  • Now a JavaScript Error is thrown instead of calling response.error. Parse will handle transforming this into a response for us.
  • Deleting a ‘Post’ or ‘Comment’ involves grabbing the object first, then destroying it. By using ‘await’, the destroy method can access the saved object outside of a block.

That completes all necessary refactoring for migration. That’s to say, if you do up until this Step, congrats! Your code will run on Back4App Parse 3.1!

Step 5: Advanced Tricks (optional)

The following changes are optional but can shorten your code significantly. I find they reduce boilerplate.

You probably noticed a lot of manual checks for parameters, or the authenticated user. These ensure that strange behaviour doesn’t occur. For example, we’ve decided our ‘Post’ object needs text, so if no ‘text’ parameter is passed, the object will get saved without it. One way to prevent this is to check that text was passed.

But manual checks can be time consuming and inelegant. In this section, we take advantage of object destructuring to implicitly complete these checks.

You should see the documentation above if you don’t know what destructuring is. But in short, it allows you to turn an object’s property into a variable with very concise syntax.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// This
var obj = {
  hi: true,
  bye: false
};
var hi = obj.hi;
var bye = obj.bye;

// Is equivalent to this:
var obj = {
  hi: true,
  bye: false
};
var { hi, bye } = obj;

console.log(hi);
// true
console.log(bye);
// false

Destructuring is definitely less verbose than manual assignment. It also allows you to declare parameter variables on the fly which is really nice:

1
2
3
Parse.Cloud.define('posts:get', async ({ params: { id } }) => {
  // id is declared
});

When combined with an es2015 notation for object initialization we can really optimize our parameter checks.

Basically we can do this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Iterates through object's keys. Makes sure, for each key, the value is set
const AssertParams = parameter_obj => {
  for (var key of Object.keys(parameter_obj)) {
    if (typeof parameter_obj[key] === 'undefined')
      throw new Error(`Missing parameter ${key}`);
  }
};

var obj = {
  hi: true,
  bye: false
};
var { hi, undef, bye } = obj; // undef will be undefined
var check_1 = { hi, bye };
var check_2 = { hi, undef };

// check = { hi : true, no : undefined }
AssertParams(check_1); // passes
AssertParams(check_2); // throws error

So for our Parse code we can do this:

1
2
3
4
5
6
7
8
9
10
// Snippet of advanced code refactoring. See full code
//  here in the link at the top of this step
Parse.Cloud.define('posts:delete', async ({ user, params: { id } }) => {
  // Makes sure user is authenticated, and id parameter is passed
  AssertParams({ user, id });
  const post = await new Parse.Query(POST).get(id, {
    useMasterKey: true
  });
  return post.destroy({ useMasterKey: true });
});

If this is daunting, don’t fret. The full code example might help.

In short, ‘AssertParams’ is a utility function for throwing an error if a value is undefined. We can check for parameters in one motion by combining object destructuring and es2015 object initialization.

This removes eight or nine manual checks which start to become unsightly after a while.

Step 6: upgrading on Back4App

Once you have migrated your code, you must do two more things to have it running on Back4App.

  1. Upgrade your Back4App server version
  2. Upload your new cloud code.

I’ve briefly mentioned the first step before. All you need to do is log into Back4App, and go to your app’s dashboard. From there, just select “Server Settings” on the left, followed by the “Settings” button on the “Manage Parse Server” card. You may then select the Parse Server 3.1.1 radio button and hit “Save”.

Last but not least, you can upload your code via the Back4App CLI, or via your dashboard using the manual upload. See this guide if you need more information on this step.

Notes on launching new version

If your code is complex, it might be a good idea to run some tests prior to doing these two steps. Back4App has some guides on how to do this here and here.

Finally, it’s important to note that these changes are breaking. Using the code we wrote on a 2.x Parse Server will fail, and using 2.x code on a 3.1 server will also fail. Therefore, you must make sure to upgrade your Back4App parse version right when you upload your upgraded Cloud Code. If you have many users and are concerned about the code upload and version upgrade being slightly out of sync, you can do something like this.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const serverVersion =
  Parse.CoreManager.get('VERSION') === 'js1.11.1' ? 'OLD' : 'NEW';

if (serverVersion === 'NEW') {
  Parse.Cloud.define('posts:get', async ({ params: { id } }) => {
    AssertParams({ id });
    // Needs a post ID
    return new Parse.Query(POST).get(id, {
      useMasterKey: true
    });
  });
} else if (serverVersion === 'OLD') {
  // Old definition here
}

This code dynamically figures out the version and defines Cloud Functions based on that. After uploading this, you would change to 3.1, and then you could re-upload the code with the old part removed. Including the check at first ensures there isn’t a point where your code will crash. Since you can upgrade and upload within a couple seconds, it’s usually not necessary.

Conclusion

At this guide, we demonstrated simply how to migrate your Cloud Code to the Parse 3.1 release. Remember to bump your Back4App version to 3.1 after you make these changes.

Also importantly, this guide demonstrated an improved syntax that Parse 3.1 leverages. Our refactorings cut the codebase from ~160 lines to ~90, and made it much more readable. For actual applications this will pay dividends.