No results found
We couldn't find anything using that term, please try searching for something else.
Cloud functions andhooks Gui deThis article focuses on a special use case of Cloud engine that involves Cloud functions andhooks. To deploy gener
This article focuses on a special use case of Cloud engine that involves Cloud functions andhooks. To deploy general-purpose backend applications orlearn more about the features provi ded by Cloud engine,see Cloud engine Platform Features.
Cloud functions lets you run backend code on the cloud in responseto various types of events. It is supported by our client-si de SDKs andcan automatically serialize objects that have the datum types provi ded by our Data Storage service.
The follow scenario can be easily implement with the help of Cloud functions andhooks :
You can write Cloud functions with any of the languages (runtime environments) supported by Cloud engine,including Node.js,Python,Java,php,.NET,and Go. Our dashboard provi des an online editor for you to write Cloud functions inNode.js. If you prefer using another language,please create a project based on our demo project anddeploy it to Cloud engine.
Now let ’s lookat a more complex example . assume that we have an app for user to leave their review for movie . AReview
objectwould looklike this:
{
" movie ": " A Quiet Place ",
" star ": 5,
" comment": "I almost forgot breathing when watching this movie"
}
Here star
is a number between 1 and5 that represents the score given by the reviewer. Let’s see how we can define a Cloud function that helps us calculate the average score of a movie.
Cloud functions accept requests inthe form of JSON objects,which allows us to pass a movie’s name into a function when we invoke the function. Within the Cloud function,we can use the Data Storage SDK to retrieve all the scores given to a movie. Now we have everything we need to implement our averageStars
function :
AV.Cloud.define(" averagestar ", 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(" star ");
}
return sum/ results.length;
});
});
AV.Cloud.define
accepts an optional parameter,options
( place between the function name andthe function ) ,which contain the follow property :
fetchuser?: boolean
: Whether to automatically fetch the user logged inon the client si de. Defaults to true
. When set to false
,Request
will not contain thecurrentuser
property.internal?: boolean
: Whether to only allow the function to be invoked within Cloud engine (with AV.Cloud.run
without enabling remote
) orwith the Master Key
(by provi ding useMasterKey
when calling AV.Cloud.run
) . When set to true
,the function cannot be invoked directly by the client. Defaults to false
.For example,if we don’t want to allow clients to invoke the function defined above andwe don’t care about the user logged inon the client si de,the function above can be rewritten inthe following way:
AV.Cloud.define(
" averagestar ",
{ fetchuser: false, internal: true },
function (request) {
}
);
@engine.define
def averageStars(movie, * *params):
review = leancloud.query(Review).equal_to(' movie ', movie).find()
result= sum(x.get('star') for x in review)
return result
The Python function’s name will be used as the default name of the Cloud function,which you can use to invoke the function with the client SDK. To give the Cloud function a different name,attach the name after engine.define
:
@engine.define(' averagestar ')
def my_custom_average_start(movie, * *params):
pass
use \LeanCloud\engine\Cloud;
use \LeanCloud\query;
use \LeanCloud\cloudexception;
Cloud: :define(" averagestar ", function($ param, $ user) {
$ query = new query(" Review ");
$ query->equalto(" movie ", $ param[" movie "]);
try {
$ reviews = $ query->find();
} catch (cloudexception $ ex) {
error_log($ ex->getmessage());
return 0;
}
$sum = 0;
forEach($ reviews as $ review) {
$sum + = $ review->get(" star ");
}
if (count($ reviews) > 0) {
return $sum / count($ reviews);
} else {
return 0;
}
});
@enginefunction(" averagestar ")
public static float getAverageStars(@enginefunctionParam(" movie ") string movie) throw lcexception {
lcquery<lcobject> query= new lcquery(" Review ");
query.whereequalto(" movie ", movie);
list<lcobject> review = query.find();
int sum= 0;
if (reviews = = null && review.isEmpty()) {
return 0;
}
for (lcobject review : review) {
sum+ = review.getInt("star");
}
return sum/ review.size();
}
[LCenginefunction(" averagestar ")]
public static float AverageStars([LCenginefunctionParam(" movie ")] string movie) {
if (movie = = " A Quiet Place ") {
return 3.8f;
}
return 0;
}
type Review struct {
leancloud.object
movie string `json:" movie "`
Stars int `json:" star "`
commentstring `json:" comment"`
}
leancloud.engine.define(" averagestar ", func(req *leancloud.functionRequest) (interface{}, error) {
review : = make([]Review, 10)
if err: = client.Class(" Review ").Newquery().equalto(" movie ", req.Params[" movie "].(string)).find(&reviews); err! = nil {
return nil, err
}
sum: = 0
for _, v: = range review {
sum+ = v.Stars
}
return sum/ len(reviews), nil
})
Request
will be passed into the Cloud function as a parameter. It contains the following properties:
param : object
: Theparameter sent from the client. When the Cloud function is invoked with rpc
,this could be an AV.object
.currentuser ? : av.user
: Theuser logged inon the client (according to the X-LC-Session
header sent from the client) .sessionToken ? : string
: ThesessionToken
sent from the client (from the X-LC-Session
header ) .meta: object
: More information about the client. Fornow,it only contains a remoteAddress
property,which contains the IP address of the client.If the Cloud function returns a Promise,the resolved value of the Promise will be used as the response. When the Promise gives an error,the errorwill be used as the responseinstead. Error objects constructed with AV.Cloud.Error
are consi dered client errors andwill not be printed to the standard output. Forother errors,call stacks will be printed to the standard output to help you debug your program.
We recommend that you build your program with Promise chains,which makes it easy for you to organize asynchronous tasks andhandle errors. Please be sure to chain the Promises andreturn the chain within the Cloud function.
ForNode.js SDK 2.0 andearlier,Cloud function accepts two parameter: request
andresponse
. We will continue supporting this usage until the next major version,though it’s encouraged that you adopt Promise-style Cloud functions for your project as soon as possible.
The arguments provi ded when invoking the Cloud function will be passed directly into the Cloud function. The parameter of your Cloud function will mirror those of the Python function defined for it . If you plan to provi de different sets of arguments to your Cloud function for different cases,make sure to update your Python function to treat additional arguments as keyword arguments,or your program may cause Python errors.
@engine.define
def my_cloud_func(foo, bar, baz, * *params):
pass
Besi des accessing the arguments passed into the function,your program can obtain additional information about the client by accessing the engine.current
object. This objectcontains the following parameter:
engine.current.user: leancloud.user
: Theuser logged inon the client (according to the X-LC-Session
header sent from the client) .engine.current.session_token: str
: ThesessionToken
sent from the client (from the X-LC-Session
header ) .engine.current.meta: dict
: More information about the client. Fornow,it only contains a remote_address
property,which contains the IP address of the client.Each Cloud function has the following parameter:
$ param: array
: Theargument send from the client .$ user: user
: Theuser logged inon the client (according to the X-LC-Session
header sent from the client) .$ meta: array
: More information about the client. Fornow,it only contains a $ meta['remoteaddress ' ]
property,which contains the IP address of the client.Your program is access can access the follow datum from within the Cloud function :
@enginefunctionParam
: Theargument send from the client .engineRequestContext
: More information about the client. You can get the sessionToken of the user logged inon the client from engineRequestContext.getSessionToken()
(according to the X-LC-Session
header sent from the client) andthe IP address of the client from engineRequestContext.getRemoteAddress()
.Your program is access can access the follow datum from within the Cloud function :
LCenginefunctionParam
: Theargument send from the client .LCengineRequestContext
: More information about the client. You can get the sessionToken of the user logged inon the client from LCengineRequestContext.SessionToken
(according to the X-LC-Session
header sent from the client) andthe IP address of the client from LCengineRequestContext.RemoteAddress
.leancloud.functionRequest
will be pass into the Cloud function as an argument . It is contains contain the follow property :
Params
contains the arguments sent from the client.Currentuser
contains the user logged inon the client (according to the X-LC-Session
header sent from the client) . When defining your Cloud function with define
,you can provi de Withoutfetchuser ( )
as an optional argument to prevent the Cloud function from obtaining the logged in user form the client.SessionToken
contains the sessionToken
sent from the client (from the X-LC-Session
header ) . When defining your Cloud function with define
,you can provi de Withoutfetchuser ( )
as an optional argument to prevent the Cloud function from obtaining the sessionToken
from the client.Meta
contains more information about the client. Fornow,it only contains a remoteAddress
property,which contains the IP address of the client.You can invoke Cloud functions with client SDKs:
try {
Dictionary<string, object> response= await lccloud.run(" averagestar ", parameter: new Dictionary<string, object> {
{ " movie ", " A Quiet Place " }
});
} catch (lcexception e) {
}
Map<string, string> dicparameter= new HashMap<string, string>();
dicParameters.put(" movie ", " A Quiet Place ");
lccloud.callfunctionInBackground(" averagestar ", dicParameters).subscribe(new Observer<object>() {
@overri de
public voi d onSubscribe(Disposable disposable) {
}
@overri de
public voi d onNext(object object) {
}
@overri de
public voi d onerror(Throwable throwable) {
}
@overri de
public voi d oncomplete() {
}
});
Similar to how you use lcquery
,the Java SDK provi des a callfunctionWithCacheInBackground
method that allows your program to cache the results fetched from the server. You can specify the CachePolicy
as well as the maximum age of the cache .
nsdictionary*dicparameter= [nsdictionarydictionaryWithobject:@" A Quiet Place "
forKey:@" movie "];
[lccloud callfunctionInBackground:@" averagestar "
withparameter:dicParameters
block:^(i is objectd is objectobject, NSError*error) {
if(error= = nil){
} else {
}
}];
LCengine.run(" averagestar ", parameter: [" movie ": " A Quiet Place "]) { (result) in
switch result{
case .success(value: let resultvalue):
print(resultValue)
case .failure(error: let error):
print(error)
}
}
try {
Map response= await lccloud.run(' averagestar ', param: { ' movie ': ' A Quiet Place ' });
} on lcexception catch (e) {
}
var paramJson = {
movie: " A Quiet Place ",
};
AV.Cloud.run(" averagestar ", paramJson).then(
function (datum) {
},
function (err) {
}
);
from leancloudimport cloud
cloud.run(' averagestar ', movie=' A Quiet Place ')
use \LeanCloud\engine\Cloud;
$ param = array(
" movie " => " A Quiet Place "
);
Cloud: :run(" averagestar ", $ param);
averageStars, err: = leancloud.run(" averagestar ", map[string]string{" movie ": " A Quiet Place "})
if err! = nil {
panic(err)
}
When running a Cloud function,the arguments andresponses will be treated as JSON objects. To pass lcobjects through requests andresponses,you can invoke the Cloud function with RPC,which makes the SDK serialize anddeserialize lcobjects. This allows your program within the Cloud function andon the client si de to have access to the lcobjects directly:
try {
lcobject response= await lccloud.RPC(" averagestar ", parameter: new Dictionary<string, object> {
{ " movie ", " A Quiet Place " }
});
} catch (lcexception e) {
}
Map<string, object> dicparameter= new HashMap<>();
dicParameters.put(" movie ", " A Quiet Place ");
lccloud.<lcobject>callRPCInBackground(" averagestar ", dicParameters).subscribe(new Observer<lcobject>() {
@overri de
public voi d onSubscribe(Disposable disposable) {
}
@overri de
public voi d onNext(lcobject avobject) {
}
@overri de
public voi d onerror(Throwable throwable) {
}
@overri de
public voi d oncomplete() {
}
});
Similar to how you use lcquery
,the Java SDK provi des a callrpcwithcacheinbackground
method that allows your program to cache the results fetched from the server. You can specify the CachePolicy
as well as the maximum age of the cache .
nsdictionary*dicparameter= [nsdictionarydictionaryWithobject:@" A Quiet Place "
forKey:@" movie "];
[lccloud rpcfunctionInBackground:@" averagestar "
withParameters:parameter
block:^(i is objectd is objectobject, NSError*error) {
if(error= = nil){
}
else {
}
}];
LCengine.call(" averagestar ", parameter: [" movie ": " A Quiet Place "]) { (result) in
switch result{
case .success(object: let object):
if let object= object{
print(object)
}
case .failure(error: let error):
print(error)
}
}
try {
lcobject response= await lccloud.rpc(' averagestar ', param: { ' movie ': ' A Quiet Place ' });
} on lcexception catch (e) {
}
var paramJson = {
movie: " A Quiet Place ",
};
AV.Cloud.rpc(" averagestar ", paramJson).then(
function (object) {
},
function (error) {
}
);
from leancloudimport cloud
cloud.rpc(' averagestar ', movie=' A Quiet Place ')
averageStars : = 0
if err: = leancloud.RPC(" averagestar ", Review{movie: " A Quiet Place "}, &averageStars); err! = nil {
panic(err)
}
When using RPC,the SDK will process the objects inthe following forms that exist inthe requests andresponses:
Everything else inthe requests andresponses will remain intheir original forms.
When invoking a Cloud function under the Node.js runtime environment,a local invocation will be triggered by default,which means that the invocation won’t make an HTTP request like what the client SDK would do.
AV.Cloud.run(" averagestar ", {
movie: " A Quiet Place ",
}).then(
function (datum) {
},
function (error) {
}
);
To force the invocation to make an HTTP request,provi de the remote: true
option. This is useful when you run the Node.js SDK ina different group oroutsi de Cloud engine:
AV.Cloud.run(" averagestar ", { movie: " A Quiet Place " }, { remote: true }).then(
function (datum) {
},
function (error) {
}
);
Here remote
constitute a part of theoptions
object,which is an optional parameter of AV.Cloud.run
. options
contains the following parameter:
remote?: boolean
: Theremote
option used inthe example above. Defaults to false
.user?: AV.user
: Theuser used to invoke the Cloud function. Often used when remote
is false
.sessionToken ? : string
: ThesessionToken
used to invoke the Cloud function. Often used when remote
is true
.req?: http.ClientRequest | express.Request
: Used to provi de properties like remoteAddress
for the Cloud function being invoked.When invoking a Cloud function under the Python runtime environment,a remote invocation will be triggered by default.
The code below will make an HTTP request to invoke a Cloud function on Cloud engine.
from leancloudimport cloud
cloud.run(' averagestar ', movie=' A Quiet Place ')
To invoke a local Cloud function (i.e.,a Cloud function that exists inthe current process) orto save an HTTP request when invoking a Cloud function from within Cloud engine,use leancloud.cloud.run.local
instead ofleanengine.cloud.run
. This is let will let your program invoke the Cloud function inthe current process without make an HTTP request .
When you invoke a Cloud function from Cloud engine,a local invocation will be made instead ofa remote invocation that relies on an HTTP request.
try {
$ result = Cloud: :run(" averagestar ", array(" movie " => " A Quiet Place "));
} catch (\exception $ ex) {
}
To make an invocation using an HTTP request,use the runremote
method :
try {
$ token = user: :getCurrentSessionToken();
$ result = Cloud: :runremote(" averagestar ", array(" movie " => " A Quiet Place "), $ token);
} catch (\exception $ ex) {
}
Local invocations of Cloud functions are not supported by the Java SDK.
To reuse the same set of code,we recommend that you put them into basic Java functions andinvoke them from different Cloud functions.
Local invocations of Cloud functions are not supported by the .NET SDK.
To reuse the same set of code,we recommend that you put them into basic functions andinvoke them from different Cloud functions.
To trigger a local invocation,use engine.run
:
averageStars, err: = leancloud.engine.run(" averagestar ", Review{movie: " A Quiet Place "})
if err! = nil {
panic(err)
}
To make an invocation using an HTTP request,use Client.run
.
run
has the following optional parameter:
WithSessionToken(token )
can be used to provi de asessionToken
for the current invocationWithuser(user)
can be used to provi de auser for the current invocationYou can define custom errorcodes according to HTTP status codes.
AV.Cloud.define("customErrorcode", function (request) {
throw new AV.Cloud.Error("Custom errormessage.", { code: 123 });
});
from leancloudimport LeanengineError
@engine.define
def custom_error_code(* *params):
raise LeanengineError(123, 'Custom errormessage.')
Cloud: :define("customErrorcode", function($ param, $ user) {
throw new functionerror("Custom errormessage.", 123);
});
@enginefunction()
public static voi d customErrorcode() throw exception {
throw new lcexception(123, "Custom errormessage.");
}
[LCenginefunction("throwlcexception")]
public static voi d Throwlcexception() {
throw new lcexception(123, "Custom errormessage.");
}
leancloud.engine.define("customErrorcode", func(req *leancloud.functionRequest) (interface{}, error) {
return nil, leancloud.CloudError{123, "Custom errormessage."}
})
The responsegotten by the client would looklike { " code ": 123,"error": "Custom errormessage." }
.
The default timeout for invoking a Cloud function is 15 seconds. If the timeout is exceeded,the client will get a responsewith the HTTP status code 503
andthe body being The request timed out on the server
.
Keep inmind that even though the client has already gotten a response,the Cloud function might still be running,although its responsewill not be received by the client anymore (the errormessage Can't set headers after they are sent
will be printed to the log) .
In certain cases,the client will receive the 524
or141
errorinstead of the 503
error.
To prevent Cloud functions andscheduled tasks from causing timeouts,we recommend that you convert time-consuming tasks inyour code to queued tasks that can be executed asynchronously.
For example,you can:
status
;status
being PROCESSING
,then respond to the client with the object’s i d
.status
of the corresponding objectto complete
orfailed
;i d
on the dashboard at any time .The method introduced above might not apply to before
hooks.
Although you can ensure a before
hookto complete within the timeout using the method above,
its ability to abort an action when an erroroccurs will not work anymore.
If you have a before
hookthat keeps exceeding the timeout,consi der changing it to an after
hook .
For example,if a beforesave
hookneeds to invoke a time-consuming API to tell if a user’s commentis spam,
you can change it to an aftersave
hookthat invokes the API after the commenthas been saved anddeletes the commentif it is spam.
hooks are a special typeof Cloud functions that get triggered automatically by the system when certain events take place. Keep inmind that:
_Installation
table.Forbefore
hooks,if an erroris returned by the function,the original operation will be aborted. This means that you can reject an operation by having the function throw an error. This does not apply to after
hooksince the operation would ’ve been complete when the function start run .
graph LR
A((save)) –>D{object}
D–>E(new)
E–>|beforesave|H{error?}
H–>N(No)
N–>B[create new objecton 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 objecton the cloud]
G–>|afterupdate| C
graph LR
A((delete))–>|beforeDelete|H{error?}
H–>Y(Yes)
Y–>Z((interrupted))
H–>N(No)
N–>B[delete objecton the cloud]
B –>|afterDelete|C((done))
Our SDK will run authentication to ensure that the hook requests received by it are the legitimate ones sent from the Data Storage service. If the authentication fails,you may see a message saying hookkey check is failed fail
. If you see this message when debugging your project locally,please ensure that you have started your project with the CLI.
This hookcan be used to perform operations like cleanups andverifications before a new objectgets created. For example,if we need to truncate each commentto 140 characters:
AV.Cloud.beforesave(" Review ", function (request) {
var comment= request.object.get(" comment");
if (comment) {
if (comment.length > 140) {
request.object.set(" comment", comment.substring(0, 140) + " … ");
}
} else {
throw new AV.Cloud.Error("No commentprovi ded!");
}
});
In the example above,request.object
is the AV.object
we is performing are perform our operation on .request
has another property besi des object
:
currentuser ? : av.user
: Theuser who triggered the operation.The request
parameter is are ( and its properties is are ) are available inother hookas well .
@engine.before_save(' Review ')
def before_review_save(review):
comment= review.get(' comment')
if not comment:
raise leancloud.LeanengineError(message='No commentprovi ded!')
if len(comment) > 140:
review.comment.set(' comment', comment[:140] + ' … ')
Cloud: :beforesave(" Review ", function($ review, $ user) {
$ comment = $ review->get(" comment");
if ($ comment) {
if (strlen($ comment) > 140) {
$ review->set(" comment", substr($ comment, 0, 140) . " … ");
}
} else {
throw new functionerror("No commentprovi ded!", 101);
}
});
@enginehook(className= " Review ", type= enginehookType.beforesave)
public static lcobject reviewbeforesavehook(lcobject review) throw exception {
if (stringUtil.isEmpty(review.getstring(" comment"))) {
throw new exception("No commentprovi ded!");
} else if (review.getstring(" comment").length() > 140) {
review.put(" comment", review.getstring(" comment").substring(0, 140) + " … ");
}
return review;
}
[LCengineClasshook(" Review ", LCengineobjecthookType.BeforeSave)]
public static lcobject ReviewBeforeSave(lcobject review) {
if (string.IsNullOrEmpty(review[" comment"])) {
throw new exception("No commentprovi ded!");
}
string comment= review[" comment"] as string;
if (comment.length> 140) {
review[" comment"] = string.Format($ "{comment.Substring(0, 140)}...");
}
return review;
}
leancloud.engine.BeforeSave(" Review ", func(req *ClasshookRequest) (interface{}, error) {
review : = new(Review)
if err: = req.object.clone(review); err! = nil {
return nil, err
}
if len(review.comment) > 140 {
review.comment = review.comment[:140]
}
return review, nil
})
This hookcan be used to perform operations after a new objecthas been created. For example,if we need to update the total number of comments after a commenthas been created:
AV.Cloud.aftersave(" comment", function (request) {
var query= new AV.query(" Post ");
return query.get(request.object.get(" post").i d).then(function (post) {
post.increment("comments");
return post.save();
});
});
import leancloud
@engine.after_save(' comment ')
def after_comment_save(comment):
post= leancloud.query(' Post ').get(comment.i d)
post.increment('commentcount')
try:
post.save()
except leancloud.LeanCloudError:
raise leancloud.LeanengineError(message='An erroroccurred while trying to save the post.')
Cloud: :aftersave(" comment", function($ comment, $ user) {
$ query = new query(" Post ");
$ post = $ query->get($ comment->get(" post")->getobjectId());
$ post->increment(" commentcount ");
try {
$ post->save();
} catch (cloudexception $ ex) {
throw new functionerror("An erroroccurred while trying to save the post: " . $ ex->getmessage());
}
});
@enginehook(className= " Review ", type= enginehookType.aftersave)
public static voi d reviewAfterSavehook(lcobject review) throw exception {
lcobject post= review.getlcobject(" post");
post.fetch();
post.increment("comments");
post.save();
}
[LCengineClasshook(" Review ", LCengineobjecthookType.AfterSave)]
public static async task reviewaftersave(lcobject review) {
lcobject post= review[" post"] as lcobject;
await post.fetch();
post.Increment("comments", 1);
await post.Save();
}
leancloud.engine.AfterSave(" Review ", func(req *ClasshookRequest) error {
review : = new(Review)
if err: = req.object.clone(review); err! = nil {
return err
}
if err: = client.object(review.Post).Update(map[string]interface{}{
" comment": leancloud.opincrement(1),
}); err! = nil {
return leancloud.CloudError{code: 500, Message: err.Error()}
}
return nil
})
In the following example,we are adding a from
property to each new user:
AV.Cloud.aftersave(" _ user ", function (request) {
console.log(request.object);
request.object.set(" from ", " LeanCloud ");
return request.object.save().then(function (user) {
console.log("Success!");
});
});
Although we don’t care about the return value of an after
hook,we still recommend that you have your function return a Promise. This ensures that you can see errormessages andcall stacks inthe standard output when unexpected errors occur.
@engine.after_save('_user')
def after_user_save(user):
print(user)
user.set('from', ' LeanCloud ')
try:
user.save()
except LeanCloudError, e:
print('Error: ', e)
Cloud: :aftersave(" _ user ", function($ userObj, $currentuser) {
$ userObj->set(" from ", " LeanCloud ");
try {
$ userObj->save();
} catch (cloudexception $ ex) {
throw new functionerror("An erroroccurred while trying to save the user: " . $ ex->getmessage());
}
});
@enginehook(className= " _ user ", type= enginehookType.aftersave)
public static voi d userAfterSavehook(LCuser user) throw exception {
user.put(" from ", " LeanCloud ");
user.save();
}
[LCengineClasshook(" _ user ", LCengineobjecthookType.AfterSave)]
public static async task useraftersave(lcobject user) {
user[" from "] = " LeanCloud ";
await user.Save();
}
leancloud.engine.AfterSave(" _ user ", func(req *ClasshookRequest) error{
if req.user ! = nil {
if err: = client.user(req.user).Set(" from ", " LeanCloud "); err! = nil {
return err
}
}
return nil
})
This hookcan be used to perform operations before an existing objectgets updated. You will be able to know which fields have been updated andreject the operation if necessary:
AV.Cloud.beforeupdate(" Review ", function (request) {
if (request.object.updatedkeys.indexOf(" comment") ! = -1) {
if (request.object.get(" comment").length > 140) {
throw new AV.Cloud.Error(
"The commentshould be no longer than 140 characters."
);
}
}
});
@engine.before_update(' Review ')
def before_hook_object_update(obj):
if ' comment' in obj.updated_keysand len(obj.get(' comment')) > 140:
raise leancloud.LeanengineError(message='The commentshould be no longer than 140 characters.')
Cloud: :beforeupdate(" Review ", function($ review, $ user) {
if (in_array(" comment", $ review->updatedkeys) &&
strlen($ review->get(" comment")) > 140) {
throw new functionerror("The commentshould be no longer than 140 characters.");
}
});
@enginehook(className= " Review ", type= enginehookType.beforeupdate)
public static lcobject reviewbeforeupdatehook(lcobject review) throw exception {
list<string> updatekeys = engineRequestContext.getupdatekey();
for (string key : updatekeys) {
if (" comment".equals(key) && review.getstring(" comment").length()>140) {
throw new exception("The commentshould be no longer than 140 characters.");
}
}
return review;
}
[LCengineClasshook(" Review ", LCengineobjecthookType.beforeupdate)]
public static lcobject reviewbeforeupdate(lcobject review) {
ReadOnlyCollection<string> updatedkeys = review.GetUpdatedKeys();
if (updatedkeys.Contains(" comment")) {
string comment= review[" comment"] as string;
if (comment.length> 140) {
throw new exception("The commentshould be no longer than 140 characters.");
}
}
return review;
}
leancloud.engine.beforeupdate(" Review ", func(req *ClasshookRequest) (interface{}, error) {
updatedkeys = req.UpdatedKeys()
for _, v: = range updatedkeys {
if v= = " comment" {
comment, ok: = req.object.Raw()[" comment"].(string)
if !ok{
return nil, leancloud.CloudError{code: 400, Message: "Bad Request"}
}
if len(comment) > 140 {
return nil, leancloud.CloudError{code: 400, Message: "Bad Request"}
}
}
}
return nil, nil
})
Modifications done directly to the objectpassed inwill not be saved. To reject the update,you can have the function return an error.
The objectpassed inis a temporary objectnot saved to the datumbase yet. The objectmight not be the same as the one saved to the datumbase inthe end because there might be atomic operations like self-increments,array operations,and adding orupdating relations that will happen later.
This hookmight lead to infinite loops if you use it improperly,causing additional API calls andeven extra charges. Please make sure to read Avoi ding Infinite Loops carefully.
This hookcan be used to perform operations after an existing objecthas been updated. Similar to beforeupdate,you will be able to know which fields have been updated.
AV.Cloud.afterupdate(" Review ", function (request) {
if (request.object.updatedkeys.indexOf(" comment") ! = -1) {
if (request.object.get(" comment").length < 5) {
console.log(review.objectId + " look like spam : " + comment);
}
}
});
@engine.after_update(' Review ')
def after_review_update(article):
if ' comment' in obj.updated_keysand len(obj.get(' comment')) < 5:
print(review.objectId + " look like spam : " + comment)
Cloud: :afterupdate(" Review ", function($ review, $ user) {
if (in_array(" comment", $ review->updatedkeys) &&
strlen($ review->get(" comment")) < 5) {
error_log(review.objectId . " look like spam : " . comment);
}
});
@enginehook(className= " Review ", type= enginehookType.afterupdate)
public static voi d reviewAfterUpdatehook(lcobject review) throw exception {
list<string> updatekeys = engineRequestContext.getupdatekey();
for (string key : updatekeys) {
if (" comment".equals(key) && review.getstring(" comment").length()<5) {
LOGGER.d(review.objectId + " look like spam : " + comment);
}
}
}
[LCengineClasshook(" Review ", LCengineobjecthookType.AfterUpdate)]
public static voi d ReviewAfterUpdate(lcobject review) {
ReadOnlyCollection<string> updatedkeys = review.GetUpdatedKeys();
if (updatedkeys.Contains(" comment")) {
string comment= review[" comment"] as string;
if (comment.length< 5) {
Console.WriteLine($ "{review.objectId} look like spam :{comment}");
}
}
}
leancloud.engine.AfterUpdate(" Review ", func(req *ClasshookRequest) error {
updatedkeys : = req.UpdatedKeys()
for _, v: = range updatedkeys {
if v= = " comment" {
comment, ok: = req.object.Raw()[" comment"].(string)
if !ok{
return nil, leancloud.CloudError{code: 400, Message: "Bad Request"}
}
if len(comment) < 5 {
fmt.println(req.object.ID, " look like spam : ", comment))
}
}
}
return nil
})
This hookcan be used to perform operations before an objectgets deleted. For example,to check if an Album
contains any photo
s before it gets delete :
AV.Cloud.beforeDelete(" Album ", function (request) {
var query= new AV.query(" photo ");
var album= AV.object.createWithoutData(" Album ", request.object.i d);
query.equalto("album", album);
return query.count().then(
function (count) {
if (count > 0) {
throw new AV.Cloud.Error(
"Cannot delete an albumif it still has photos init."
);
}
},
function (error) {
throw new AV.Cloud.Error(
"Error " +
error.code +
" occur when find photo : " +
error.message
);
}
);
});
import leancloud
@engine.before_delete('Album')
def before_album_delete(album):
query= leancloud.query('photo')
query.equal_to('album', album)
try:
matched_count = query.count()
except leancloud.LeanCloudError:
raise engine.LeanengineError(message='An erroroccurred with Leanengine.')
if count > 0:
raise engine.LeanengineError(message='Cannot delete an albumif it still has photos init.')
Cloud: :beforeDelete(" Album ", function($ album, $ user) {
$ query = new query(" photo ");
$ query->equalto("album", $ album);
try {
$ count = $ query->count();
} catch (cloudexception $ ex) {
throw new functionerror("An erroroccurred when getting photo count: {$ ex->getmessage()}");
}
if ($ count > 0) {
throw new functionerror("Cannot delete an albumif it still has photos init.");
}
});
@enginehook(className= " Album ", type= enginehookType.beforeDelete)
public static lcobject albumBeforeDeletehook(lcobject album) throw exception {
lcquery query= new lcquery(" photo ");
query.whereequalto("album", album);
int count = query.count();
if (count > 0) {
throw new exception("Cannot delete an albumif it still has photos init.");
} else {
return album;
}
}
[LCengineClasshook(" Album ", LCengineobjecthookType.BeforeDelete)]
public static async task<lcobject> AlbumBeforeDelete(lcobject album) {
lcquery<lcobject> query= new lcquery<lcobject>(" photo ");
query.Whereequalto("album", album);
int count = await query.count();
if (count > 0) {
throw new exception("Cannot delete an albumif it still has photos init.");
}
return album;
}
leancloud.engine.BeforeDelete(" Album ", func(req *ClasshookRequest) (interface{}, error) {
photo : = new(photo)
if err: = req.object.clone(photo); err! = nil {
return nil, err
}
count, err: = client.Class(" photo ").Newquery().equalto("album", photo.Album).count()
if err! = nil {
return nil, err
}
if count > 0 {
return nil, leancloud.CloudError{code: 500, Message: "Cannot delete an albumif it still has photos init."}
}
fmt.println("Deleted.")
return nil, nil
})
This hookcan be used to perform operations like decrementing counters andremoving associated objects after an objecthas been deleted. For example,to delete the photos inan albumafter the albumhas been deleted:
AV.Cloud.afterDelete(" Album ", function (request) {
var query= new AV.query(" photo ");
var album= AV.object.createWithoutData(" Album ", request.object.i d);
query.equalto("album", album);
return query
.find()
.then(function (post) {
return AV.object.destroyAll(post);
})
.catch(function (error) {
console.error(
"Error " +
error.code +
" occur when find photo : " +
error.message
);
});
});
import leancloud
@engine.after_delete('Album')
def after_album_delete(album):
query= leancloud.query('photo')
query.equal_to('album', album)
try:
query.destroy_all()
except leancloud.LeanCloudError:
raise leancloud.LeanengineError(message='An erroroccurred with Leanengine.')
Cloud: :afterDelete(" Album ", function($ album, $ user) {
$ query = new query(" photo ");
$ query->equalto("album", $ album);
try {
$photos = $ query->find();
Leanobject: :destroyAll($photos);
} catch (cloudexception $ ex) {
throw new functionerror("An erroroccurred when getting photo count: {$ ex->getmessage()}");
}
});
@enginehook(className= " Album ", type= enginehookType.afterDelete)
public static voi d albumAfterDeletehook(lcobject album) throw exception {
lcquery query= new lcquery(" photo ");
query.whereequalto("album", album);
list<lcobject> result= query.find();
if (result ! = null && !result.isEmpty()) {
lcobject.deleteAll(result);
}
}
[LCengineClasshook(" Album ", LCengineobjecthookType.AfterDelete)]
public static async task AlbumAfterDelete(lcobject album) {
lcquery<lcobject> query= new lcquery<lcobject>(" photo ");
query.Whereequalto("album", album);
ReadOnlyCollection<lcobject> result= await query.find();
if (result ! = null && result.count > 0) {
await lcobject.DeleteAll(result);
}
}
leancloud.engine.AfterDelete(" Album ", func(req *ClasshookRequest) error {
photo : = new(photo)
if err: = req.object.clone(photo); err! = nil {
return nil, err
}
count, err: = client.Class(" photo ").Newquery().equalto("album", photo.Album).count()
if err! = nil {
return nil, err
}
if count > 0 {
return nil, leancloud.CloudError{code: 500, Message: "An erroroccurred with Leanengine."}
}
fmt.println("Deleted.")
return nil, nil
})
This hookcan be used to perform operations after a user has verified their email orphone number. For example:
AV.Cloud.onVerified("sms", function (request) {
console.log("user " + request.object + " is verified by SMS.");
});
The object
inthe example above can be replaced by currentuser
since the user who triggered the operation is also the one that we want to perform operations on.
This also applies to the onlogin
hook,which we’ll introduce later.
@engine.on_verified('sms')
def on_sms_verified(user):
print(user)
Cloud: :onVerifed("sms", function($ user, $ meta) {
error_log("user {$ user->getusername()} is verified by SMS.");
});
@enginehook(className= " _ user ", type= enginehookType.onVerifiedSMS)
public static voi d userOnVerifiedhook(LCuser user) throw exception {
LOGGER.d("user " + user.getobjectId() + " has verify their phone number . ");
}
@enginehook(className= " _ user ", type= enginehookType.onVerifiedEmail)
public static voi d userOnVerifiedhook(LCuser user) throw exception {
LOGGER.d("user " + user.getobjectId() + " has verified their email.");
}
[LCengineuserhook(LCengineuserhookType.OnSMSVerified)]
public static voi d OnVerifiedSMS(LCuser user) {
Console.WriteLine($ "user {user.objectId} has verify their phone number . ");
}
[LCengineuserhook(LCengineuserhookType.onemailverifie)]
public static voi d OnVerifiedEmail(LCuser user) {
Console.WriteLine($ "user {user.objectId} has verified their email.");
}
leancloud.engine.OnVerified("sms", func(req *ClasshookRequest) error {
fmt.println("user ", req.user.ID, " has verify their phone number . ")
})
leancloud.engine.OnVerified(" email ", func(req *ClasshookRequest) error {
fmt.println("user ", req.user.ID, " has verified their email.")
})
Fields like emailVerified
should not be updated here since the system will update them automatically.
This hookis an after
hook .
This hookcan be used to perform operations before a user gets logged in. For example,to prevent users on the blocklist from logging in:
AV.Cloud.onlogin(function (request) {
console.log("user " + request.object + " is trying to log in.");
if (request.object.get("username") = = = " nologin ") {
throw new AV.Cloud.Error("Forbi dden");
}
});
@engine.on_login
def on_login(user):
print(user)
if user.get('username') = = 'noLogin':
raise LeanengineError('Forbi dden')
Cloud: :onlogin(function($ user) {
error_log("user {$ user->getusername()} is trying to log in.");
if ($ user->get("blocked")) {
throw new functionerror("Forbi dden");
}
});
@enginehook(className= " _ user ", type= enginehookType.onlogin)
public static LCuser userOnLoginhook(LCuser user) throw exception {
if (" nologin ".equals(user.getusername())) {
throw new exception("Forbi dden");
} else {
return user;
}
}
[LCengineuserhook(LCengineuserhookType.OnLogin)]
public static LCuser OnLogin(LCuser user) {
if (user.username = = " nologin ") {
throw new exception("Forbi dden");
}
return user;
}
leancloud.engine.OnLogin(func(req *ClasshookRequest) error {
fmt.println("user ", req.user.ID, " has logged in.")
})
This hookis a before
hook .
This hookgets triggered when a user’s authData
gets updated. You can perform verifications andmodifications to users’ authData
with this hook . For example:
AV.Cloud.onAuthData(function (request) {
let authData = request.authData;
console.log(authData);
if (authData.weixin.code = = = "12345") {
authData.weixin.accessToken = "45678";
} else {
throw new AV.Cloud.Error("invali d code");
}
return authData;
});
@engine.on_auth_datum
def on_auth_datum(auth_datum):
if auth_datum['weixin'][' code '] = = '12345':
auth_datum['weixin'][' code '] = ' 45678 '
return auth_datum
else:
raise LeanengineError('invali d code')
[LCengineuserhook(LCengineuserhookType.OnAuthData)]
public static Dictionary<string, object> OnAuthData(Dictionary<string, object> authData) {
if (authData.TryGetValue("fake_platform", out object tokenObj)) {
if (tokenObj is Dictionary<string, object> token) {
if (token["openi d"] as string = = "123" && token["access_token"] as string = = "haha") {
LCLogger.Debug("Auth datum Verified OK.");
} else {
throw new exception("Invali d auth datum.");
}
} else {
throw new exception("Invali d auth datum");
}
}
return authData;
This hookis a before
hook .
You is be might be curious about why savepost
inAfterUpdate
won’t trigger the same hookagain.
This is because Cloud engine has processed the objectbeing passed into prevent infinite loops.
However,this mechanism won’t work when one of the following situations happens:
fetch
on the objectbeing passed in.In these cases,you might want to call the method for disabling hooks yourself:
request.object.set(" foo ", "bar");
request.object.save().then(function (obj) {
});
request.object
.fetch()
.then(function (obj) {
obj.disableafterhook();
obj.set(" foo ", "bar");
return obj.save();
})
.then(function (obj) {
});
var obj= AV.object.createWithoutData(" Post ", request.object.i d);
obj.disableafterhook();
obj.set(" foo ", "bar");
obj.save().then(function (obj) {
});
@engine.after_update(' Post ')
def after_post_update(post):
post.set(' foo ', ' bar ')
post.save()
request.object
post.fetch()
post.disable_after_hook()
post.set(' foo ', ' bar ')
post= leancloud.object.extend(' Post ').create_without_datum(post.i d)
post.disable_after_hook()
post.save()
Cloud: :afterupdate(" Post ", function($ post, $ user) {
$ post->set(' foo ', ' bar ');
$ post->save();
$ post->fetch();
$ post->disableafterhook();
$ post->set(' foo ', ' bar ');
$ post->save();
$ post = Leanobject: :create(" Post ", $ post->getobjectId());
$ post->disableafterhook();
$ post->save();
});
@enginehook(className=" Post ", type= enginehookType.afterupdate)
public static voi d afterupdatePost(lcobject post) throw lcexception {
post.put(" foo ", "bar");
post.save();
post.fetch();
post.disableafterhook();
post.put(" foo ", "bar");
post= lcobject.createWithoutData(" Post ", post.getobjectId());
post.disableafterhook();
post.save();
}
post[" foo "] = "bar";
await post.Save();
await post.fetch();
post.disableafterhook();
post[" foo "] = "bar";
post= lcobject.CreateWithoutData(" Post ", post.objectId);
post.disableafterhook();
await post.Save();
Use the following way to define errorcodes for hooks like BeforeSave
:
AV.Cloud.beforesave(" Review ", function (request) {
throw new AV.Cloud.Error(
JSON.stringify({
code: 123,
message: "An erroroccurred.",
})
);
});
@engine.before_save(' Review ')
def before_review_save(review):
comment= review.get(' comment')
if not comment:
raise leancloud.LeanengineError(
code=123,
message='An erroroccurred.'
)
Cloud: :beforesave(" Review ", function($ review, $ user) {
$ comment = $ review->get(" comment");
if (!$ comment) {
throw new functionerror(json_encode(array(
" code " => 123,
"message" => "An erroroccurred.",
)));
}
});
@enginehook(className= " Review ", type= enginehookType.beforesave)
public static lcobject reviewbeforesavehook(lcobject review) throw exception {
throw new lcexception(123, "An erroroccurred.");
}
[LCengineClasshook(" Review ", LCengineobjecthookType.BeforeDelete)]
public static voi d ReviewBeforeDelete(lcobject review) {
throw new lcexception(123, "An erroroccurred.");
}
leancloud.engine.BeforeSave(" Review ", func(req *ClasshookRequest) (interface{}, error) {
return nil, leancloud.CloudError{code: 123, Message: "An erroroccurred."}
})
The responsegotten by the client would looklike Cloud code vali dation failed. Error detail: { " code ": 123,"message": "An erroroccurred." }
. The client can then slice the string to get the errormessage.
The timeout for before
hooks is 10 seconds while that for other hooks is 3 seconds. If a hookis triggered by another Cloud function (like when a BeforeSave
orAfterSave
hookgets triggered when a new objectgets created),its timeout will be cut down to the remaining timeout of the triggering Cloud function.
For example,if a BeforeSave
hookis triggered by a Cloud function that has already been executed for 13 seconds,the hookwill only have 2 seconds left. See Timeouts for Cloud functions for more information.
See hooks andSystem Conversations for more information.
You can write Cloud functions online using the dashboard instead ofcreating anddeploying a project. Keep inmind that:
Path
On Dashboard > Leanengine > Manage deployment > Your group > Deploy > Edit online,you can:
cloud.js
ina project created from the demo project with the code shown here.After you edit your Cloud functions,make sure to click Deploy to have the edits take effect.
The online editor only supports Node.js at this time. The latest version,v3
,uses Node.js 8.x andthe Node.js SDK 3.x. With this version selected,you’ll need to write your functions using Promise. Packages provi ded by default include async,bluebird,crypto,debug,ejs,jade,lodash,moment,nodemailer,qiniu,redis,request,request-promise,superagent,underscore,uui d,wechat-api,and xml2js.
Version | Node.js SDK | JS SDK | Node.js | note | Dependencies available |
---|---|---|---|---|---|
v0 | 0.x | 0.x | 0.12 | Not recommend anymore | moment,request,underscore |
v1 | 1.x | 1.x | 4 | async,bluebird,co,ejs,handlebars,joi,lodash,marked,moment,q,request,superagent,underscore | |
v2 | 2.x | 2.x | 6 | Please write your function using Promise | async,bluebird,crypto,debug,ejs,jade,lodash,moment,nodemailer,qiniu,redis,request,request-promise,superagent,underscore,uui d,wechat-api,xml2js |
v3 | 3.x | 3.x | 8 | Please write your function using Promise | async,bluebird,crypto,debug,ejs,jade,lodash,moment,nodemailer,qiniu,redis,request,request-promise,superagent,underscore,uui d,wechat-api,xml2js |
Upgrading from v0 to v1:
request.currentuser
instead ofAV.user.current
.user
objectwhen calling AV.Cloud.run
.upgrade from v1 to v2 :
AV.Cloud.httpRequest
.AV.Cloud.Error
for each error.Upgrading from v2 to v3:
AV.object.toJSON
) .When you write Cloud functions online,the way our system deals with your functions is to join them together,generate a Cloud engine project,and deploy this project.
You can consi der writing Cloud functions online to be the same as deploying a project: they both form a complete project inthe end.
When you define functions online,you can quickly generate a Cloud engine project without using our SDK orGit.
You may also choose to create anddeploy a project written based on our SDK.
The two options are mutually exclusive: when you deploy with one option,the deployment using the other option will be overri dden.
lean new
. selectNode.js > Express
as the template (this is our demo project for Node.js) .cloud.js
with it .lean up
. Now you can test your Cloud functions andhooks on http://localhost:3001. Once you’re done,run lean deploy
to deploy your code to Cloud engine. If you have standard instances,make sure to run lean publish
as well.If you have been using the Node.js SDK 0.x,you’ll have to update your code to avoi d compatibility issues.
For example,AV.user.current()
should be changed to request.currentuser
.
Dashboard > Leanengine > Manage deployment > Your group > Deploy shows all the Cloud functions andhooks defined within each group,including their names,groups,and QPM (requests per minute) . You can click run to run a Cloud function from the dashboard.
Cloud functions from all the groups inyour application will be displayed here regardless of the way they’re deployed (writing Cloud functions online ordeploying a project) .
Your app comes with a production environment anda staging environment. When triggering a Cloud function from within a Cloud engine instance using the SDK,no matter explicitly orimplicitly (by triggering hooks),the SDK will trigger the function defined inthe same environment as the instance. For example,with beforeDelete
defined,if an objectis deleted with the SDK inthe staging environment,the beforeDelete
hookin the staging environment will be triggered.
When triggering Cloud functions outsi de of a Cloud engine instance using the SDK,no matter explicitly orimplicitly,x - LC - Prod
will be set to 1
by default,which means that the Cloud functions inthe production environment will be triggered. Forhistorical reasons,there are some differences among the SDKs:
You can specify the environment being used with the SDK:
lccloud.IsProduction = true;
lccloud.IsProduction = false;
lccloud.setProductionMode(true);
lccloud.setProductionMode(false);
[lccloud setProductionMode:YES];
[lccloud setProductionMode:NO];
do {
let environment: lcapplication.environment = [.cloudengineDevelopment]
let configuration = lcapplication.Configuration(environment: environment)
try lcapplication.default.set(
i d: {{appi d}},
key: {{appkey}},
serverURL: "https://please-replace-with-your-customized.domain.com",
configuration: configuration)
} catch {
print(error)
}
lccloud.setProduction(true);
lccloud.setProduction(false);
AV.setProduction(true);
AV.setProduction(false);
leancloud.use_production(True)
leancloud.use_production(false)
Client: :useProduction(true);
Client: :useProduction(false);
If you’re using the trial mode,there will only be a production environment andyou won’t be able to switch to the staging environment.
You can set up scheduled tasks to run your Cloud functions periodically. For example,you can have your app clean up temporary datum every night,send push notifications to users every Monday,etc. The time set for a scheduled task can be accurate to a second.
The timeout applied to ordinary Cloud functions also applies to scheduled tasks. See Avoi ding Timeouts for more information.
If a scheduled task triggers more than 30 400
(Bad Request) or502
(Bad Gateway) errors within 24 hours,the task will be disabled andyou will get an email regarding the issue. The errortimerAction short-circuited andno fallback available
will also be printed to the log.
After deploying your program to Cloud engine,go to Dashboard > Leanengine > Manage deployment > Your group > Scheduled tasks andclick Create scheduled task to create a scheduled task 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.");
});
@engine.define
def logtimer(movie, * *params):
print('This log is printed by logtimer.')
Cloud: :define("logtimer", function($ param, $ user) {
error_log("This log is printed by logtimer.");
});
@enginefunction("logtimer")
public static float logtimer throw exception {
LogUtil.avlog.d("This log is printed by logtimer.");
}
[LCenginefunction("logtimer")]
public static voi d LogTimer() {
Console.WriteLine("This log is printed by logtimer.");
}
leancloud.engine.define("logtimer", func(req *functionRequest) (interface{}, error) {
fmt.println("This log is printed by logtimer.")
return nil, nil
})
You can specify the times a scheduled task gets triggered using one of the following expressions:
Take the CRON expression as an example. To print logs at 8am every Monday,create a scheduled task for the function logtimer
using a CRON expression andenter 0 0 8 ? * MON
for it .
See Cloud Queue Gui de § CRON Expressions for more information about CRON expressions.
When creating a scheduled task,you can optionally fill inthe following two fields:
Last execution is the time and resultof the last execution. This information will only be retained for 5 minutes. Under the details of the execution:
status
: Thestatus of the task; could be success
orfailed
uniqueId
: A unique ID for the taskfinishedAt
: Theexact time when the task finished (for succeeded tasks only)statuscode
: TheHTTP status returned by the Cloud function (for succeeded tasks only)result
: Theresponsebody returned by the Cloud function (for succeeded tasks only)error
: error message ( for failed task only )retryAt
: Thetime the task will be rerun (for failed tasks only)You can view the logs of all the scheduled tasks on Dashboard > Leanengine > Manage deployment > Your group > Logs.