Cloud function is a sub-module of LeanEngine that allows you to run functions on the cloud in response to the requests made by clients. Before you continue, make sure you have read LeanEngine Overview.
When developing your application, you may need to write logic that:
Are shared by multiple platforms (like iOS, Android, and web) and you wish to write them only once.
Need to be modified frequently (like the sorting rules of a list) but you don't want to release a new version of client each time you make a change.
Demand high network traffic or computing power (like producing statistics on a huge table) which you don't want to run on clients.
Need to be triggered when certain events happen (hooking). For example, when a user deletes an account, you may wish to delete the entries in other tables that are related to this account as well.
Need to bypass certain restrictions set by ACL.
Need to be performed routinely. For example, you may wish to clean up inactive accounts every month.
With cloud function, you can deploy these types of logic written in any language (JavaScript, Python, PHP, or Java) on the cloud and have LeanEngine run them for you.
If you have no idea how to deploy your project to LeanEngine, take a look at LeanEngine Quick Start.
Other Languages
This guide uses Node.js as an example, but LeanEngine supports many other languages as well. You can choose the one you are familiar with for development:
LeanEngine offers two environments for each app: production environment and staging environment. When calling cloud functions within LeanEngine instances using SDK, no matter explicitly or implicitly (by triggering hooks), the SDK uses the function defined in the same environment as the instance. For example, if beforeDelete hook is defined and an object is deleted with SDK in the staging environment, the beforeDelete hook in the staging environment will be triggered.
When calling cloud functions outside of LeanEngine instances using SDK, no matter explicitly or implicitly, X-LC-Prod is set to be 1 by default, which means that the production environment will be used. For historical reasons, there are some differences between each SDK:
For Node.js, PHP, and Java SDKs, the production environment will always be used by default.
For Python SDK, when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used.
For Java example projects java-war-getting-started and spring-boot-getting-started, when debugging locally with lean-cli, the staging environment will be used if it exists. Otherwise, the production environment will be used (same as Python SDK).
You can specify the environment being used with SDK:
Apps with only trial instances would only have production environments. Please do not attempt to switch to staging environments.
Cloud Functions
In this example, a simple cloud function named hello is defined in cloud.js. By doing so, clients running on all platforms will be able to call it and get the return value of it. The computing process of the function is done on the cloud side rather than on the client side, so there will be less burden on the clients.
Now let's look into a more complex example.
Imagine that you have an app that lets users review the movies they have watched. An object containing a single review of a movie may look like this:
{
"movie": "Despicable Me",
"stars": 5,
"comment": "These minions are so cute. I wish they are real and I can have them in my home!"
}
stars is the score given by the user, ranging from 1 to 5. If you want to obtain the average score of Despicable Me, one thing you can do is to have the client search for all the reviews of this movie and calculate the average score on the device. However, this requires all the reviews of this movie to be fetched to the client, which leads to unnecessary network traffic. With cloud function, you can simply have the client pass the name of the movie to the cloud and receive the calculated score only.
Cloud functions accept parameters in JSON objects which we can include the name of the movie in. All the methods defined in LeanStorage JavaScript SDK can be used on LeanEngine, so we can write the cloud function averageStars like this:
AV.Cloud.define('averageStars', function (request) {
var query = new AV.Query('Review');
query.equalTo('movie', request.params.movie);
return query.find().then(function (results) {
var sum = 0;
for (var i = 0; i < results.length; i++) {
sum += results[i].get('stars');
}
return sum / results.length;
});
});
Parameters and Return Values
A Request will be passed into a cloud function as a parameter. Each Request contains the following properties:
params: object: The parameters sent from the client; might be an AV.Object when the function is called with rpc.
currentUser?: AV.User: The user logged in at the client side (according to the header X-LC-Session).
sessionToken?: string: The sessionToken sent from the client (according to the header X-LC-Session).
meta: object: Other information about the client, including remoteAddress which is the IP address of the client.
AV.Cloud.define also takes in an optional parameter options (between the function name and the actual function) which has the following properties:
fetchUser: boolean: Whether to automatically get the user information from the client; defaults to true. When set to false, Request will not have the currentUser property.
internal: boolean: Whether to only allow the function to be called within LeanEngine (using AV.Cloud.run without enabling remote) or with Master Key (by passing in useMasterKey to AV.Cloud.run) and prevent it from being called directly by the client; defaults to false.
For example, if we don't allow a client to call the function above directly and the function does not need the user information, we can rewrite the function in this way:
AV.Cloud.define('averageStars', { fetchUser: false, internal: true }, function (request) {
// Same definition as above
});
If the cloud function returns a Promise, the client will receive the response once the Promise is resolved. If an error occurred in the Promise, the client will receive the error as the response. An exception made by AV.Cloud.Error will be considered as a client-side error and will not be output as an error message. The error messages for other exceptions will be printed out accordingly.
We recommend that you chain Promises together to make it easy to process tasks asynchronously and handle errors. Make sure to have the cloud function return the chained Promises. You can read Promises/A+ Proposal to learn more about Promises.
Calling Cloud Functions with SDK
You can call cloud functions with any LeanCloud SDK:
// Construct the dictionary to be passed to the cloud
NSDictionary *dicParameters = [NSDictionary dictionaryWithObject:@"Despicable Me"
forKey:@"movie"];
// Call averageStars with parameters
[AVCloud callFunctionInBackground:@"averageStars"
withParameters:dicParameters
block:^(id object, NSError *error) {
if(error == nil){
// Success; object is the returned value from the cloud function
} else {
// Error handling
}
}];
var paramsJson = {
movie: "Despicable Me"
};
AV.Cloud.run('averageStars', paramsJson).then(function (data) {
// Success; data is the returned value from the cloud function
}, function (err) {
// Error handling
});
from leancloud import cloud
cloud.run('averageStars', movie='Despicable Me')
use \LeanCloud\Engine\Cloud;
$params = array(
"movie" => "Despicable Me"
);
Cloud::run("averageStars", $params);
// Construct the parameters to be passed to the cloud
Map<String, String> dicParameters = new HashMap<String, String>();
dicParameters.put("movie", "Despicable Me");
// Call averageStars with parameters
AVCloud.callFunctionInBackground("averageStars", dicParameters).subscribe(new Observer<AVObject>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(AVObject avObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {
}
});
// Java SDK also supports caching results returned by cloud function, similar to AVQuery.
// For example, the following calls will use cached results if available,
// and the cached results will expire in 30 seconds (30000 ms).
AVCloud.callFunctionWithCacheInBackground("averageStars", dicParameters, AVQuery.CachePolicy.CACHE_ELSE_NETWORK, 30000)
.subscribe(new Observer<Object>() {
@Override
public void onSubscribe(Disposable disposable) {}
@Override
public void onNext(Object object) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed.
}
@Override
public void onComplete() {}
});
try {
Map response = await LCCloud.run('averageStars', params: { 'movie': '夏洛特烦恼' });
// deal with results
} on LCException catch (e) {
// deal with exceptions
}
You can call the cloud functions defined by AV.Cloud.define with AV.Cloud.run:
AV.Cloud.run('averageStars', {
movie: 'Despicable Me'
}).then(function (data) {
// Success; data is the returned value from the cloud function
}, function (error) {
// Error handling
});
By doing so, a local function call will be triggered without initiating an HTTP request. If you want to call the cloud function through an HTTP request, add remote: true as an option. This will be helpful when you use Node.js SDK outside of a LeanEngine environment (including calling cloud functions defined in other groups):
AV.Cloud.run('averageStars', { movie: 'Despicable Me' }, { remote: true }).then(function (data) {
// Success; data is the returned value from the cloud function
}, function (error) {
// Error handling
});
Here remote is passed into AV.Cloud.run as a property of an optional parameter options. options has the following properties:
remote?: boolean: The same remote as used in the example above; defaults to false.
user?: AV.User: The user used to run the function (often used when remote is false).
sessionToken?: string: The sessionToken used to run the function (often used when remote is true).
req?: http.ClientRequest | express.Request: Used to provide remoteAddress to the function being called.
Calling Cloud Functions with RPC
By calling cloud functions with RPC, LeanEngine will automatically serialize the HTTP response body and the SDK will get the response in the format of AV.Object:
var paramsJson = {
movie: "Despicable Me"
};
AV.Cloud.rpc('averageStars', paramsJson).then(function (object) {
// Success
}, function (error) {
// Error handling
});
from leancloud import cloud
cloud.rpc('averageStars', movie='Despicable Me')
// Not supported yet
// Construct parameters
Map<String, Object> dicParameters = new HashMap<>();
dicParameters.put("movie", "Despicable Me");
AVCloud.<AVObject>callRPCInBackground("averageStars", dicParameters).subscribe(new Observer<AVObject>() {
@Override
public void onSubscribe(Disposable disposable) {
}
@Override
public void onNext(AVObject avObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {
}
});
// cached version (see "Calling Cloud Functions with SDK" section above)
AVCloud.<AVObject>callRPCWithCacheInBackground("averageStars", dicParameters, AVQuery.CachePolicy.CACHE_ELSE_NETWORK, 30000)
.subscribe(new Observer<AVObject>() {
@Override
public void onSubscribe(Disposable disposable) {}
@Override
public void onNext(AVObject avObject) {
// succeed.
}
@Override
public void onError(Throwable throwable) {
// failed
}
@Override
public void onComplete() {}
});
try {
LCObject response = await LCCloud.rpc('averageStars', params: { 'movie': '夏洛特烦恼' });
// deal with results
} on LCException catch (e) {
// deal with exceptions
}
Error Codes
You can customize error codes for cloud functions in accordance with HTTP status codes.
AV.Cloud.define('errorCode', function (request) {
return AV.User.logIn('NotThisUser', 'P@ssword');
});
The client will receive { "code": 211, "error": "Could not find the user." } from the function above.
AV.Cloud.define('customErrorCode', function (request) {
throw new AV.Cloud.Error('Custom error message.', { code: 123 });
});
The client will receive { "code": 123, "error": "Custom error message." } from the function above.
Timeouts
The time limit for a cloud function to be processed is 15 seconds. If the cloud function does not make a response after this, HTTP error 503 will be triggered with the error message The request timed out on the server. An error log like LeanEngine: /1.1/functions/<cloudFunc>: function timeout (15000ms) will be printed on the server side. Keep in mind that even after the client already receives the error message, the function may still be running, but the return value of the function will not be able to be sent to the client and an error log like Can't set headers after they are sent will be printed.
Handling Timeouts
We recommend that you have your application handle tasks asynchronously to avoid timeouts.
You may:
Create a new class in LeanStorage with a field named status.
For each new task, create an object with this class and mark status to be ongoing. End the request by sending the id of this task back to the client:
return new Promise((resolve, reject) => {
resolve(id);
});
After the task is completed, change its status to be completed or failed.
You can check the status of a task at any time with its id.
However, this usually does not make sense for before hooks.
Although asynchronous before hooks will not trigger timeout errors,
they also cannot interrupt the operation.
If you cannot optimize execution time for before hooks,
you have to use after hooks instead.
For example, to filter fake comments, one beforeSave hook needs to call a time-consuming third-party NLP API,
which may cause a timeout.
As a workaround, you can use an afterSave hook to call the third party after the comment is saved.
If that comment turns out to be a fake one, then delete it afterward.
Hooking
A hook can be automatically triggered when certain events happen (like before or after saving or updating an object). Keep in mind that:
Importing data on the dashboard will not trigger any hooks.
Dead loops may be caused if hooks are not appropriately defined.
Hooks cannot be applied to _Installation table.
For hooks starting with before (including onLogin), if an exception occurs inside the function, the data operation will be terminated. Therefore, you can reject certain data operations by having functions throw an error. For hooks starting with after (including onVerified), such exception will not terminate the data operation because the operation is already completed before the function is executed.
graph LR
A((save)) -->D{object}
D-->E(new)
E-->|beforeSave|H{error?}
H-->N(No)
N-->B[create new object on the cloud]
B -->|afterSave|C((done))
H-->Y(Yes)
Y-->Z((interrupted))
D-->F(existing)
F-->|beforeUpdate|I{error?}
I-->Y
I-->V(No)
V-->G[update existing object on the cloud]
G-->|afterUpdate|C
graph LR
A((delete))-->|beforeDelete|H{error?}
H-->Y(Yes)
Y-->Z((interrupted))
H-->N(No)
N-->B[delete object on the cloud]
B -->|afterDelete|C((done))
To ensure that hooks are triggered internally by LeanStorage services, our SDK will verify the source of each request. If the verification fails, the error message Hook key check failed will be returned. If you see such error message when debugging locally, make sure you are using our command-line interface for debugging.
beforeSave
You can perform an operation before an object is saved, like data verification and pre-processing. For example, a comment on a movie may be too long to be displayed on the client and needs to be cut off to 140 characters:
AV.Cloud.beforeSave('Review', function (request) {
var comment = request.object.get('comment');
if (comment) {
if (comment.length > 140) {
// Cut off and add '…' in the end
request.object.set('comment', comment.substring(0, 140) + '…');
}
} else {
// Return an error without saving the object
throw new AV.Cloud.Error('No comment provided!');
}
});
Here request.object is the AV.Object being operated. request has another property beside object:
currentUser?: AV.User: The user triggering the operation.
object and currentUser can be found in the request of other hooks as well.
afterSave
You can perform an operation after an object is saved. For example, to update the total number of comments after a comment is created:
AV.Cloud.afterSave('Comment', function (request) {
var query = new AV.Query('Post');
return query.get(request.object.get('post').id).then(function (post) {
post.increment('comments');
return post.save();
});
});
Even though the return value of such hook is not important, we still recommend that you have the function return a Promise so that error logs can be printed out when exceptions occur.
beforeUpdate
You can perform an operation before an object is updated. You will be able to know which fields are updated, or reject the data operation:
AV.Cloud.beforeUpdate('Review', function (request) {
// Check the length of comment if it is updated
if (request.object.updatedKeys.indexOf('comment') != -1) {
if (request.object.get('comment').length > 140) {
// Reject the update if comment is too long
throw new AV.Cloud.Error('Your comment cannot be longer than 140 characters.');
}
}
});
Do not attempt to modify request.object since changes made to it will not be saved to LeanStorage. If you want to reject the change, you can have the function throw an error.
The object passed into the function is a temporary object and may be different from the one saved to LeanStorage in the end (which may have atomic operations applied to).
afterUpdate
Dead loops may be triggered if this hook is not defined properly. This may lead to extra API calls or even extra bills. See Preventing Dead Loops for more details.
You can perform an operation after an object is updated. You will be able to know which fields are updated (same as beforeUpdate).
AV.Cloud.afterUpdate('Review', function(request) {
if (request.object.updatedKeys.indexOf('comment') != -1) {
if (request.object.get('comment').length < 5) {
console.log(review.ObjectId + " seems like a spam: " + comment)
}
}
});
Preventing Dead Loops
You might be wondering why we can modify and save the post object in the afterUpdate hook without triggering this hook again. This is because LeanEngine automatically identifies and pre-processes all the request.object objects passed in by a hook to prevent the hook to be triggered again.
However, if the following situations happen, you still need to handle them by yourself:
You called fetch on the request.object objects passed in.
You constructed the request.object objects passed in by yourself with methods like AV.Object.createWithoutData().
To prevent such objects from triggering certain hooks, you can call object.disableBeforeHook() or object.disableAfterHook() on them:
// Directly modifying and saving the object will not trigger afterUpdate
request.object.set('foo', 'bar');
request.object.save().then(function (obj) {
// Your logic here
});
// Use disableAfterHook if you called fetch on the object
request.object.fetch().then(function (obj) {
obj.disableAfterHook();
obj.set('foo', 'bar');
return obj.save();
}).then(function (obj) {
// Your logic here
});
// Use disableAfterHook if you constructed the object by yourself
var obj = AV.Object.createWithoutData('Post', request.object.id);
obj.disableAfterHook();
obj.set('foo', 'bar');
obj.save().then(function (obj) {
// Your logic here
});
beforeDelete
You can perform an operation before an object is deleted. For example, before an album is deleted, check if there are any photos in it:
AV.Cloud.beforeDelete('Album', function (request) {
// See if there is a Photo belonging to this Album
var query = new AV.Query('Photo');
var album = AV.Object.createWithoutData('Album', request.object.id);
query.equalTo('album', album);
return query.count().then(function (count) {
if (count > 0) {
// The delete operation will be aborted
throw new AV.Cloud.Error('Cannot delete an album if it still has photos in it.');
}
}, function (error) {
throw new AV.Cloud.Error('Error ' + error.code + ' occurred when finding photos: ' + error.message);
});
});
afterDelete
You can perform an operation after an object is deleted. For example, when an album is being deleted, instead of checking if there are any photos left, we directly delete all the photos in it:
AV.Cloud.afterDelete('Album', function (request) {
var query = new AV.Query('Photo');
var album = AV.Object.createWithoutData('Album', request.object.id);
query.equalTo('album', album);
return query.find().then(function (posts) {
return AV.Object.destroyAll(posts);
}).catch(function (error) {
console.error('Error ' + error.code + ' occurred when finding photos: ' + error.message);
});
});
onVerified
You can perform an operation after a user's email or phone number is verified:
AV.Cloud.onVerified('sms', function (request) {
console.log('User ' + request.object + ' is verified by SMS.');
});
Here object can be replaced by currentUser as well since the object being operated is the user itself. Same for onLogin.
The first parameter is the type of verification (sms for phone number and email for email).
Keep in mind that you don't have to update fields like emailVerified with this hook since the system automatically updates them.
onLogin
You can perform an operation before a user tries to log in. For example, to prevent blocked users from logging in:
AV.Cloud.onLogin(function (request) {
// The user is not logged in yet and their information is in request.object
console.log('User ' + request.object + ' is trying to log in.');
if (request.object.get('username') === 'noLogin') {
// The user cannot log in if an error is thrown (error 401 will be returned)
throw new AV.Cloud.Error('Forbidden');
}
});
You can also verify access_token of a third party account using this hook.
Suppose there is a platform called Example with the following authData:
and the platform name is example_platform. We can verify its access_token using the method below:
AV.Cloud.onLogin(function (request) {
const authData = request.object.get("authData");
const userID = authData.example_platform.uid;
const token = authData.example_platform.access_token;
https.get(`https://example.com/applications/${userID}/tokens/${token}`, (res) => {
if (res.statusCode !== 200) {
throw new AV.Cloud.Error('Example platform token invalid');
}
}).on('error', (e) => {
console.error(e);
throw new AV.Cloud.Error('Failed to verify Example platform token');
});
});
Be aware that the code above is for demonstration only.
We omitted a lot of logic for brevity,
such as verifying token's expiration date, handling user login in other ways, and recovering from network errors.
You can perform an operation after a message is delivered to the cloud but before it is delivered to the receiver. For example, to filter out certain keywords from the message:
AV.Cloud.onIMMessageReceived((request) => {
// request.params: {
// fromPeer: 'Tom',
// receipt: false,
// groupId: null,
// system: null,
// content: '{"_lctext":"Hello!","_lctype":-1}',
// convId: '5789a33a1b8694ad267d8040',
// toPeers: ['Jerry'],
// __sign: '1472200796787,a0e99be208c6bce92d516c10ff3f598de8f650b9',
// bin: false,
// transient: false,
// sourceIP: '121.239.62.103',
// timestamp: 1472200796764
// }
let content = request.params.content;
console.log('content', content);
let processedContent = content.replace('selling drugs', '**');
// Must return the content or an error will be triggered
return {
content: processedContent
};
});
onIMReceiversOffline
You can perform an operation if a message is delivered to the receiver but the receiver is offline. For example, to slice the first 32 characters of the message as the title for the push notification:
AV.Cloud.onIMReceiversOffline((request) => {
let params = request.params;
let content = params.content;
// params.content is the content of the message
let shortContent = content;
if (shortContent.length > 32) {
shortContent = content.slice(0, 32);
}
console.log('shortContent', shortContent);
return {
pushMessage: JSON.stringify({
// Self-increase the number of unread messages
// Can be set to a fixed number
badge: "Increment",
sound: "default",
// Using development certificate
_profile: "dev",
alert: shortContent
})
}
});
onIMMessageSent
You can perform an operation after a message is delivered to the receiver. For example, to print out a log in LeanEngine:
You can perform an operation after the signature verification (if enabled) of creating a conversation is completed but before the conversation is created. For example, to print out a log in LeanEngine:
You can perform an operation after the signature verification (if enabled) of adding a member into a conversation is completed but before the member is added. This will be triggered both when the member proactively joins the conversation or is added by another member. Keep in mind that this hook will not be triggered when creating a conversation with clientIds. For example, to print out a log in LeanEngine:
You can perform an operation after the signature verification (if enabled) of removing a member from a conversation is completed but before the member is removed. This will not be triggered when the member proactively quits the conversation. For example, to print out a log in LeanEngine:
You can perform an operation before the name, custom attributes, or mention properties of a conversation (including notifications) are updated. For example, to print out a log in LeanEngine:
AV.Cloud.onIMConversationUpdate((request) => {
let params = request.params;
console.log('params:', params);
console.log('name:', params.attr.name);
// params: {
// convId: '57c9208292509726c3dadb4b',
// initBy: 'Tom',
// attr: {
// name: 'Tom and Jerry',
// type: 'public'
// }
// }
// name: Tom and Jerry
});
Error Codes for Hooks
You can define error codes for hooks like beforeSave:
AV.Cloud.beforeSave('Review', function (request) {
// Convert the object into a string with JSON.stringify()
throw new AV.Cloud.Error(JSON.stringify({
code: 123,
message: 'An error occurred.'
}));
});
The client will receive Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." } as the response. You can retrieve the error message by slicing the string.
Timeouts for Hooks
The time limit for a hook to be processed is 3 seconds. If a hook is triggered by another cloud function (like a beforeSave or afterSave triggered by saving an object), the time limit of this hook will be further limited by the time remaining.
For example, if a beforeSave is triggered by a cloud function that has already taken 13 seconds, there will be only 2 seconds left for this hook. See Timeouts.
Editing Cloud Functions Online
If you just want to use cloud functions without setting up a complete Node.js project, you can edit cloud functions on the web console. Keep in mind that:
Setting up cloud functions on the web console will override the project you deployed with Git or command-line interface.
You can only set up cloud functions (including hooks) on the web console. You cannot use other functions of an ordinary Node.js project (like web hosting or routers).
You can only use SDK and some built-in modules (see below for details). You cannot import other modules as dependencies.
Online editing only supports LeanCloud JavaScript SDK (3.x), Node.js SDK (3.x), and the following modules:
Create functions by specifying function types, function names, code in the functions, and comments. Click on Save to finish creating a function.
Deploy functions by specifying the environment and clicking on Deploy.
Preview functions by viewing all the functions in a single code snippet. You can save the snippet as cloud.js and put it in a Node.js project so you can deploy the project with the cloud functions you created.
Maintain functions by editing them, viewing version histories, and deleting them.
Make sure to click on Deploy after editing cloud functions.
Scheduled Tasks
You can set up timers to schedule your cloud functions. For example, to clean up temporary data every night, to send push notifications to users every Monday, etc. The timer can be accurate to a second.
The time restrictions applied to ordinary cloud functions also apply to scheduled functions. See Timeouts.
If a timer triggers more than 30 400 (Bad Request) or 502 (Bad Gateway) errors within the past 24 hours, the system will disable it and send a email to you regarding the issue. The error log timerAction short-circuited and no fallback available will also be printed out in the web console.
After deploying your program to LeanEngine, go to your app's Dashboard > LeanEngine > Scheduled tasks and click on Create a timer to create a timer for a cloud function. For example, if we have a function named logTimer:
AV.Cloud.define('logTimer', function (request) {
console.log('This log is printed by logTimer.');
});
You can specify the times a task gets triggered using one of the following expressions:
CRON expression
Interval in seconds
Take CRON expression as an example.
To print logs at 8am every Monday, create a timer for the function logTimer using CRON expression and enter 0 0 8 ? * MON for it.
When creating a timer, two optional options are available:
Execute Parameters: the parameters passed to the cloud function in a JSON object
Retry Policy: retry or cancel the task when it fails due to a cloud function timeout
After the timer is created, the dashboard will display Last executed and Next execution time.
Last executed is the time and result of the last execution.
Clicking on the details button will reveal more information about the task:
status: the status of the task; could be success or failed
uniqueId: a unique ID for the task
finishedAt: the exact time when the task finished (for successful tasks only)
statusCode: the HTTP status returned by the cloud function (for successful tasks only)
result: the response body returned by the cloud function (for successful tasks only)
error: error message (for failed tasks only)
retryAt: the time when the cloud will try to rerun the task (for failed tasks only)
If you want to suspend a timer temporarily, say, for debug purpose, you can click on the Disable button in the Status column.
Correspondingly, clicking on the Enable button will reactivate a suspended task.
If you want to modify or delete a task, you can click on the Edit or Delete button in the Operation column.
Special characters can be used in the following ways:
Character
Meaning
Usage
*
All values
All the values a field can have. For example, to run a task on every minute, set <minutes> to be *.
?
Unspecified value
Can be used on at most one of the two fields that accept this value. For example, to run a task on the 10th of every month regardless of what day it is, set <day-of-month> to be 10 and <day-of-week> to be ?.
-
Scope
For example, setting <hours> to be 10-12 means 10am, 11am, and 12pm.
,
Splitting multiple values
For example, setting <day-of-week> to be MON,WED,FRI means Monday, Wednesday, and Friday.
/
Interval
For example, setting <seconds> to be */15 means every 15 seconds starting from the 0th, which are the 0th, 15th, 30th, and 45th seconds.
Fields are concatenated with spaces. Values like JAN-DEC and SUN-SAT are case-insensitive (MON is the same as mon).
To illustrate:
Expression
Explanation
0 */5 * * * ?
Run a task every 5 minutes.
10 */5 * * * ?
Run a task every 5 minutes and the time to run it is always the 10th second of a minute (like 10:00:10, 10:05:10, etc.).
0 30 10-13 ? * WED,FRI
Run a task at the 10:30am, 11:30am, 12:30am, and 1:30pm of every Wednesday and Friday.
0 */30 8-9 5,20 * ?
Run a task every 30 minutes between 8am and 10am (8:00am, 8:30am, 9:00am, and 9:30am) on the 5th and 20th of every month.
The time zone followed by CRON expressions is UTC+0.
Using Master Key
Since cloud functions are running on the server side, we can assume that requests made by these functions are trustable. Therefore, you can enable global Master Key for all these requests, which will have your program ignore permission settings of classes and those done by ACL so it can access data in the cloud without any restrictions. The code below enables Master Key for your program:
// Often in server.js
AV.Cloud.useMasterKey();
If the code above is not added into your program, your program will not be able to access the data protected by ACL. You will have to specify a sessionToken to have your program perform an operation with the permission of a user:
var post = new Post();
post.save(
{ author: user },
{
// You can also use request.sessionToken (need to enable Cloud.CookieSession when using web hosting)
sessionToken: user.getSessionToken()
}
);
Or, you can enable Master Key for a single operation to ignore permission settings:
post.destroy({ useMasterKey: true });
You can also disable Master Key for a single operation with useMasterKey: false.
So when should global Master Key be enabled?
If your cloud functions mainly perform operations for global data, enabling Master Key will make these operations easier to be completed. Make sure to implement permission verification in your program carefully.
If your cloud functions mainly perform operations that are related to every single user, disabling Master Key would make your application more secure. Your program can still take in the sessionToken from each request for certain operations (like saving objects).
Node.js Cloud Function Guide
Cloud function is a sub-module of LeanEngine that allows you to run functions on the cloud in response to the requests made by clients. Before you continue, make sure you have read LeanEngine Overview.
When developing your application, you may need to write logic that:
With cloud function, you can deploy these types of logic written in any language (JavaScript, Python, PHP, or Java) on the cloud and have LeanEngine run them for you.
If you have no idea how to deploy your project to LeanEngine, take a look at LeanEngine Quick Start.
Other Languages
This guide uses Node.js as an example, but LeanEngine supports many other languages as well. You can choose the one you are familiar with for development:
Switching Environments
LeanEngine offers two environments for each app: production environment and staging environment. When calling cloud functions within LeanEngine instances using SDK, no matter explicitly or implicitly (by triggering hooks), the SDK uses the function defined in the same environment as the instance. For example, if
beforeDelete
hook is defined and an object is deleted with SDK in the staging environment, thebeforeDelete
hook in the staging environment will be triggered.When calling cloud functions outside of LeanEngine instances using SDK, no matter explicitly or implicitly,
X-LC-Prod
is set to be1
by default, which means that the production environment will be used. For historical reasons, there are some differences between each SDK:You can specify the environment being used with SDK:
Apps with only trial instances would only have production environments. Please do not attempt to switch to staging environments.
Cloud Functions
In this example, a simple cloud function named
hello
is defined incloud.js
. By doing so, clients running on all platforms will be able to call it and get the return value of it. The computing process of the function is done on the cloud side rather than on the client side, so there will be less burden on the clients.Now let's look into a more complex example.
Imagine that you have an app that lets users review the movies they have watched. An object containing a single review of a movie may look like this:
stars
is the score given by the user, ranging from 1 to 5. If you want to obtain the average score of Despicable Me, one thing you can do is to have the client search for all the reviews of this movie and calculate the average score on the device. However, this requires all the reviews of this movie to be fetched to the client, which leads to unnecessary network traffic. With cloud function, you can simply have the client pass the name of the movie to the cloud and receive the calculated score only.Cloud functions accept parameters in JSON objects which we can include the name of the movie in. All the methods defined in LeanStorage JavaScript SDK can be used on LeanEngine, so we can write the cloud function
averageStars
like this:Parameters and Return Values
A
Request
will be passed into a cloud function as a parameter. EachRequest
contains the following properties:params: object
: The parameters sent from the client; might be anAV.Object
when the function is called withrpc
.currentUser?: AV.User
: The user logged in at the client side (according to the headerX-LC-Session
).sessionToken?: string
: ThesessionToken
sent from the client (according to the headerX-LC-Session
).meta: object
: Other information about the client, includingremoteAddress
which is the IP address of the client.AV.Cloud.define
also takes in an optional parameteroptions
(between the function name and the actual function) which has the following properties:fetchUser: boolean
: Whether to automatically get the user information from the client; defaults totrue
. When set tofalse
,Request
will not have thecurrentUser
property.internal: boolean
: Whether to only allow the function to be called within LeanEngine (usingAV.Cloud.run
without enablingremote
) or with Master Key (by passing inuseMasterKey
toAV.Cloud.run
) and prevent it from being called directly by the client; defaults tofalse
.For example, if we don't allow a client to call the function above directly and the function does not need the user information, we can rewrite the function in this way:
If the cloud function returns a Promise, the client will receive the response once the Promise is resolved. If an error occurred in the Promise, the client will receive the error as the response. An exception made by
AV.Cloud.Error
will be considered as a client-side error and will not be output as an error message. The error messages for other exceptions will be printed out accordingly.We recommend that you chain Promises together to make it easy to process tasks asynchronously and handle errors. Make sure to have the cloud function return the chained Promises. You can read Promises/A+ Proposal to learn more about Promises.
Calling Cloud Functions with SDK
You can call cloud functions with any LeanCloud SDK:
Calling Cloud Functions with REST API
See LeanEngine REST API Guide.
Calling Cloud Functions on LeanEngine
You can call the cloud functions defined by
AV.Cloud.define
withAV.Cloud.run
:By doing so, a local function call will be triggered without initiating an HTTP request. If you want to call the cloud function through an HTTP request, add
remote: true
as an option. This will be helpful when you use Node.js SDK outside of a LeanEngine environment (including calling cloud functions defined in other groups):Here
remote
is passed intoAV.Cloud.run
as a property of an optional parameteroptions
.options
has the following properties:remote?: boolean
: The sameremote
as used in the example above; defaults tofalse
.user?: AV.User
: The user used to run the function (often used whenremote
isfalse
).sessionToken?: string
: ThesessionToken
used to run the function (often used whenremote
istrue
).req?: http.ClientRequest | express.Request
: Used to provideremoteAddress
to the function being called.Calling Cloud Functions with RPC
By calling cloud functions with RPC, LeanEngine will automatically serialize the HTTP response body and the SDK will get the response in the format of
AV.Object
:Error Codes
You can customize error codes for cloud functions in accordance with HTTP status codes.
The client will receive
{ "code": 211, "error": "Could not find the user." }
from the function above.The client will receive
{ "code": 123, "error": "Custom error message." }
from the function above.Timeouts
The time limit for a cloud function to be processed is 15 seconds. If the cloud function does not make a response after this, HTTP error
503
will be triggered with the error messageThe request timed out on the server
. An error log likeLeanEngine: /1.1/functions/<cloudFunc>: function timeout (15000ms)
will be printed on the server side. Keep in mind that even after the client already receives the error message, the function may still be running, but the return value of the function will not be able to be sent to the client and an error log likeCan't set headers after they are sent
will be printed.Handling Timeouts
We recommend that you have your application handle tasks asynchronously to avoid timeouts.
You may:
status
.status
to beongoing
. End the request by sending theid
of this task back to the client:status
to becompleted
orfailed
.id
.However, this usually does not make sense for before hooks. Although asynchronous before hooks will not trigger timeout errors, they also cannot interrupt the operation. If you cannot optimize execution time for before hooks, you have to use after hooks instead. For example, to filter fake comments, one
beforeSave
hook needs to call a time-consuming third-party NLP API, which may cause a timeout. As a workaround, you can use anafterSave
hook to call the third party after the comment is saved. If that comment turns out to be a fake one, then delete it afterward.Hooking
A hook can be automatically triggered when certain events happen (like before or after saving or updating an object). Keep in mind that:
_Installation
table.For hooks starting with
before
(includingonLogin
), if an exception occurs inside the function, the data operation will be terminated. Therefore, you can reject certain data operations by having functions throw an error. For hooks starting withafter
(includingonVerified
), such exception will not terminate the data operation because the operation is already completed before the function is executed.To ensure that hooks are triggered internally by LeanStorage services, our SDK will verify the source of each request. If the verification fails, the error message
Hook key check failed
will be returned. If you see such error message when debugging locally, make sure you are using our command-line interface for debugging.beforeSave
You can perform an operation before an object is saved, like data verification and pre-processing. For example, a comment on a movie may be too long to be displayed on the client and needs to be cut off to 140 characters:
Here
request.object
is theAV.Object
being operated.request
has another property besideobject
:currentUser?: AV.User
: The user triggering the operation.object
andcurrentUser
can be found in therequest
of other hooks as well.afterSave
You can perform an operation after an object is saved. For example, to update the total number of comments after a comment is created:
Or, to add a new field
from
for each new user:Even though the return value of such hook is not important, we still recommend that you have the function return a Promise so that error logs can be printed out when exceptions occur.
beforeUpdate
You can perform an operation before an object is updated. You will be able to know which fields are updated, or reject the data operation:
Do not attempt to modify
request.object
since changes made to it will not be saved to LeanStorage. If you want to reject the change, you can have the function throw an error.The object passed into the function is a temporary object and may be different from the one saved to LeanStorage in the end (which may have atomic operations applied to).
afterUpdate
Dead loops may be triggered if this hook is not defined properly. This may lead to extra API calls or even extra bills. See Preventing Dead Loops for more details.
You can perform an operation after an object is updated. You will be able to know which fields are updated (same as
beforeUpdate
).Preventing Dead Loops
You might be wondering why we can modify and save the
post
object in theafterUpdate
hook without triggering this hook again. This is because LeanEngine automatically identifies and pre-processes all therequest.object
objects passed in by a hook to prevent the hook to be triggered again.However, if the following situations happen, you still need to handle them by yourself:
fetch
on therequest.object
objects passed in.request.object
objects passed in by yourself with methods likeAV.Object.createWithoutData()
.To prevent such objects from triggering certain hooks, you can call
object.disableBeforeHook()
orobject.disableAfterHook()
on them:beforeDelete
You can perform an operation before an object is deleted. For example, before an album is deleted, check if there are any photos in it:
afterDelete
You can perform an operation after an object is deleted. For example, when an album is being deleted, instead of checking if there are any photos left, we directly delete all the photos in it:
onVerified
You can perform an operation after a user's email or phone number is verified:
Here
object
can be replaced bycurrentUser
as well since the object being operated is the user itself. Same foronLogin
.The first parameter is the type of verification (
sms
for phone number andemail
for email).Keep in mind that you don't have to update fields like
emailVerified
with this hook since the system automatically updates them.onLogin
You can perform an operation before a user tries to log in. For example, to prevent blocked users from logging in:
You can also verify
access_token
of a third party account using this hook.Suppose there is a platform called Example with the following
authData
:and the platform name is
example_platform
. We can verify itsaccess_token
using the method below:Be aware that the code above is for demonstration only. We omitted a lot of logic for brevity, such as verifying token's expiration date, handling user login in other ways, and recovering from network errors.
Hooks for LeanMessage
See LeanMessage Overview > LeanEngine Hooks for more instructions on the methods introduced below.
onIMMessageReceived
You can perform an operation after a message is delivered to the cloud but before it is delivered to the receiver. For example, to filter out certain keywords from the message:
onIMReceiversOffline
You can perform an operation if a message is delivered to the receiver but the receiver is offline. For example, to slice the first 32 characters of the message as the title for the push notification:
onIMMessageSent
You can perform an operation after a message is delivered to the receiver. For example, to print out a log in LeanEngine:
onIMConversationStart
You can perform an operation after the signature verification (if enabled) of creating a conversation is completed but before the conversation is created. For example, to print out a log in LeanEngine:
onIMConversationStarted
You can perform an operation after a conversation is created. For example, to print out a log in LeanEngine:
onIMConversationAdd
You can perform an operation after the signature verification (if enabled) of adding a member into a conversation is completed but before the member is added. This will be triggered both when the member proactively joins the conversation or is added by another member. Keep in mind that this hook will not be triggered when creating a conversation with
clientId
s. For example, to print out a log in LeanEngine:onIMConversationRemove
You can perform an operation after the signature verification (if enabled) of removing a member from a conversation is completed but before the member is removed. This will not be triggered when the member proactively quits the conversation. For example, to print out a log in LeanEngine:
onIMConversationUpdate
You can perform an operation before the name, custom attributes, or mention properties of a conversation (including notifications) are updated. For example, to print out a log in LeanEngine:
Error Codes for Hooks
You can define error codes for hooks like
beforeSave
:The client will receive
Cloud Code validation failed. Error detail: { "code": 123, "message": "An error occurred." }
as the response. You can retrieve the error message by slicing the string.Timeouts for Hooks
The time limit for a hook to be processed is 3 seconds. If a hook is triggered by another cloud function (like a
beforeSave
orafterSave
triggered by saving an object), the time limit of this hook will be further limited by the time remaining.For example, if a
beforeSave
is triggered by a cloud function that has already taken 13 seconds, there will be only 2 seconds left for this hook. See Timeouts.Editing Cloud Functions Online
If you just want to use cloud functions without setting up a complete Node.js project, you can edit cloud functions on the web console. Keep in mind that:
Online editing only supports LeanCloud JavaScript SDK (3.x), Node.js SDK (3.x), and the following modules:
In your app's Dashboard > LeanEngine > Deploy > Edit online, you can:
cloud.js
and put it in a Node.js project so you can deploy the project with the cloud functions you created.Make sure to click on Deploy after editing cloud functions.
Scheduled Tasks
You can set up timers to schedule your cloud functions. For example, to clean up temporary data every night, to send push notifications to users every Monday, etc. The timer can be accurate to a second.
The time restrictions applied to ordinary cloud functions also apply to scheduled functions. See Timeouts.
If a timer triggers more than 30
400
(Bad Request) or502
(Bad Gateway) errors within the past 24 hours, the system will disable it and send a email to you regarding the issue. The error logtimerAction short-circuited and no fallback available
will also be printed out in the web console.After deploying your program to LeanEngine, go to your app's Dashboard > LeanEngine > Scheduled tasks and click on Create a timer to create a timer for a cloud function. For example, if we have a function named
logTimer
:You can specify the times a task gets triggered using one of the following expressions:
Take CRON expression as an example. To print logs at 8am every Monday, create a timer for the function
logTimer
using CRON expression and enter0 0 8 ? * MON
for it.When creating a timer, two optional options are available:
After the timer is created, the dashboard will display Last executed and Next execution time. Last executed is the time and result of the last execution. Clicking on the details button will reveal more information about the task:
status
: the status of the task; could besuccess
orfailed
uniqueId
: a unique ID for the taskfinishedAt
: the exact time when the task finished (for successful tasks only)statusCode
: the HTTP status returned by the cloud function (for successful tasks only)result
: the response body returned by the cloud function (for successful tasks only)error
: error message (for failed tasks only)retryAt
: the time when the cloud will try to rerun the task (for failed tasks only)You can see the logs of the timer in Dashboard > LeanEngine > App logs. For example:
If you want to suspend a timer temporarily, say, for debug purpose, you can click on the Disable button in the Status column. Correspondingly, clicking on the Enable button will reactivate a suspended task. If you want to modify or delete a task, you can click on the Edit or Delete button in the Operation column.
CRON Expressions
The basic syntax of a CRON expression is:
, - * /
, - * /
, - * /
, - * ? /
, - * /
, - ? /
Special characters can be used in the following ways:
*
<minutes>
to be*
.?
<day-of-month>
to be10
and<day-of-week>
to be?
.-
<hours>
to be10-12
means 10am, 11am, and 12pm.,
<day-of-week>
to beMON,WED,FRI
means Monday, Wednesday, and Friday./
<seconds>
to be*/15
means every 15 seconds starting from the 0th, which are the 0th, 15th, 30th, and 45th seconds.Fields are concatenated with spaces. Values like
JAN-DEC
andSUN-SAT
are case-insensitive (MON
is the same asmon
).To illustrate:
0 */5 * * * ?
10 */5 * * * ?
0 30 10-13 ? * WED,FRI
0 */30 8-9 5,20 * ?
The time zone followed by CRON expressions is
UTC+0
.Using Master Key
Since cloud functions are running on the server side, we can assume that requests made by these functions are trustable. Therefore, you can enable global Master Key for all these requests, which will have your program ignore permission settings of classes and those done by ACL so it can access data in the cloud without any restrictions. The code below enables Master Key for your program:
If the code above is not added into your program, your program will not be able to access the data protected by ACL. You will have to specify a
sessionToken
to have your program perform an operation with the permission of a user:Or, you can enable Master Key for a single operation to ignore permission settings:
You can also disable Master Key for a single operation with
useMasterKey: false
.So when should global Master Key be enabled?
sessionToken
from each request for certain operations (like saving objects).See ACL Guide and Using ACL in LeanEngine for more information regarding permission settings with LeanEngine.