FlexServices
Kinvey Flex is a framework for writing server-side code for your mobile and web apps. It allows you to create low-code, lightweight Node.js microservices to use for data integrations, authentication, and general-purpose functions. Flex services utilize the Flex SDK and can consist of FlexData for data integrations, FlexFunctions for trigger-based data pre/post hooks or custom endpoints, and FlexAuth for custom authentication through Mobile Identity Connect (MIC).
You can include FlexData, FlexAuth, and FlexFunctions handlers all in the same Flex service, but it is recommended to separate them.
Types of Flex Services
Depending on where you choose to deploy you Flex Service, you can have two types of Flex services:
- Internal Flex services are projects that you deploy on the FlexService Runtime. The FlexService Runtime is cloud-based infrastructure managed by Kinvey. It is a controlled, highly-available environment that requires you to develop against specific software versions. This type of deployment is recommended for most customers as it spares you the hassle of maintaining your own servers and planning for high availability, load balancing, and scaling.
- External Flex services are projects that you deploy on server infrastructure managed by you. It is suitable to customers who require the service to run on-premises or on another type of trusted infrastructure. Subsequently, implementing high-availability, disaster recovery, load balancing, and scaling remains your responsibility.
The Flex SDK
The Kinvey Flex SDK emits events within your Node.js service when collection data is requested from your Kinvey app. These events are paired with user-defined handlers that fetch and transform data (from any number of sources) into Kinvey collections. The Flex SDK also includes modules for interacting with other Kinvey APIs (such as push notifications, email, collections access, and more).
FlexData
You can use FlexData to connect a data source that you own or manage to Kinvey. Your apps are then able to consume the data it holds as if it were stored in a Kinvey collection, using the same Kinvey SDK methods.
To connect a data source, you need to create a Node.js service that implements all the data access logic and then deploy it as either an internal or an external Flex service.
After you deploy the Flex service, you access the data from the data source by mapping it to Kinvey collections.
Similarly to any Flex service, FlexData services are Node.js projects that use the Kinvey Flex SDK. Apart from it, you can use any npm module that you see fit. The one exception are modules with external dependencies for projects deployed internally on the FlexService Runtime. For example, if you want to use the oracledb
package to connect to Oracle Database, you will need to deploy as an external Flex service because this will allow you to install the required Oracle Client alongside Node.js.
Initializing FlexData
You can access the FlexData framework through the Flex SDK's data
property.
const flexData = flex.data;
Registering Service Objects
To allow access to data in the system you are integrating with, you need to register a service object. A service object is a generic term for data objects or records on a remote system that Kinvey can interact with—for example, a database table.
Use the serviceObject()
method of the data
object to define service objects. Afterwards, you can wire up data access event handlers to those service objects.
// To register the "widgets" service object:
const widgets = flex.data.serviceObject('widgets');
After you deploy your FlexData service, you will be able to map service objects to Kinvey collections. This allows you to work with data from the FlexData service using the same APIs you use for cloud-based Kinvey collections.
You can deploy a Flex service either as an internal or an external service.
Handling Data Events
Each service object exposes data events that are invoked by Kinvey collections. The data event takes a single handler function to be executed.
Some of the data events include onInsert
, onDeleteById
, or onDeleteAll
. See the Flex API Reference for the full list.
The following example retrieves all entities in the widgets
service object:
widgets.onGetAll(callbackFunction);
Data Handler Functions
The data events take a handler function, which takes three arguments: context
, complete
, and modules
. The context
argument represents the current context of the request made to Kinvey, and complete
is a completion handler for completing the data request. The modules
argument is an object containing several libraries for accessing Kinvey functionality via your service (for more information, see the section on modules).
Accessing the Context
The context
object represents the state of the respective stage of the Kinvey request pipeline. It contains properties such as the HTTP method, the request headers, the body, the query, and so on.
See the Flex API Reference for full description.
Completing the Handler
The completion handler initiates the complete process. It follows a builder pattern for creating the handler's response. The pattern for the completion handler is complete()[.setBody(<entity>).<status>.done()|next()]
For example, a sample completion handler is:
complete(myEntity).ok().next();
complete()
method. This functionality is deprecated and will be removed in a future version of the SDK.
It provides a set of methods for altering the context of the request and a series of status functions. For the data step, the only relevant context-altering method is setBody()
, which takes an entity, an array of entities, an error description, or an Error
object.
Note that a body must be explicitly set using setBody
. The entity must be a JSON object or a JSON array for successful responses. For example:
// Sets the response to include an entity
complete().setBody({'foo':'bar'});
// Sets the response to include an array of entities
complete().setBody([{'foo':'bar'}, {'abc':'123'}]);
The setBody
method is not required. If you want to return an empty object, simply call complete()
. If you need to pass an empty array, you must call setBody()
with the empty Array.
// Sets the response to an empty object
complete();
// Sets the response to an empty array
complete().setBody([]);
For errors, you can either pass a string as the error message, or a JavaScript Error
object.
// Sets the response to an error string to be used with error status functions
complete().setBody('Record 123 was not found');
// Sets the response to an Error object to be used with error status functions
complete().setBody(new Error('Record 123 was not found');
Status functions set the valid status code for a FlexData operation. The status function also sets the body to a Kinvey-formatted error and uses the value passed into the status function or the setData()
function as the debug property, if it is present.
See the Flex API Reference for the available status functions.
For example:
// Return that the record has been created
complete().setBody(myRecord).created();
// Entity wasn't found
complete().setBody('The given entity wasn\'t found').notFound();
After the status is set, you can end the processing of the handler request with either done()
or next()
. Most requests should normally end with next()
, which continues the Kinvey request pipeline. The done()
function returns the response set in the completion handler and ends the request processing without executing any further functions.
// Continue the request chain
complete().setBody(myEntity).ok().next();
// End the request chain with no further processing
complete().ok().done();
Example
The following code provides a more complete example of how to structure a FlexData service.
const sdk = require('kinvey-flex-sdk');
sdk.service((err, flex) => {
const data = flex.data; // gets the FlexData object from the service
function getRecordById(context, complete, modules) {
let entityId = context.entityId;
let entity = null;
// Do some logic to get the entity id from the remote data store
// Assume that data is retrieved and stored in "entity" variable
// After entity is retrieved, check to see if it exists
if (typeof entity === 'undefined' || entity === null) {
return complete().notFound('The entity could not be found').next();
} else {
// return the entity
return complete().setBody(entity).ok().next();
}
}
// set the serviceObject
const widgets = data.serviceObject('widgets');
// wire up the event that we want to process
widgets.onGetById(getRecordById);
});
FlexFunctions
The FlexFunctions framework is used to execute functions invoked by hooks
or endpoints
.
Initializing FlexFunctions
You can access the FlexFunctions framework through the Flex SDK's functions
property.
const functions = flex.functions;
Registering FlexFunctions Handlers
To register a FlexFunctions handler, you define that handler and give it a name by using the register()
method of the functions
object.
The following code snippet registers the someEventHandlerName
handler:
flex.functions.register('someEventHandlerName', (context, complete, modules) => {
//eventHandler logic
});
In the Kinvey Console, when you define hooks or endpoints, you will be presented with the list of event handler names you've registered. You will be able to connect them to a collection hook or endpoint.
Handler Function
Flex functions take a handler functions with three arguments: context
, complete
, and modules
. The context
argument represents the state of the current Kinvey request context and complete
is a completion handler for completing the Flex function. The modules
argument is an object containing several libraries for accessing Kinvey functionality through your service (for more information, see the section on modules).
complete()
method. This functionality is deprecated and will be removed in a future version of the SDK.
Accessing the Context
The context
object represents the state of the respective stage of the Kinvey request pipeline. For example, for events that are executed before a data request (pre-hooks), the context contains the request body and query.
See the Flex API Reference for full description.
Completing the Function
The completion handler initiates the complete process. It follows a builder pattern for creating the handler's response. The pattern for the completion handler is complete().[setBody(<entity>).setQuery(<query>).<status>.done()|next()]
.
An example completion handler is:
complete().setBody(myEntity).setQuery(myQuery).ok().next();
It provides a set of methods for altering the context of the request and a series of status functions.
FlexFunctions support a couple of context-altering methods:
Method | Description |
---|---|
setBody() | Sets the data entity or entities to be passed to the next step of the pipeline or as the final result. Optional. Takes an entity, an array of entities, an error description, or an Error object. The entity must be a JSON object or a JSON array. |
setQuery() | Replaces the query object with an altered query object. Only useful in Before collection hook functions. |
// Sets the context to include an entity and an altered query
complete().setBody({'foo':'bar'}).setQuery({query: {'foo':'bar'}});
// Sets the response to include an array of entities
complete().setBody([{'foo':'bar'}, {'abc':'123'}]);
The setBody()
method is not required. If you want to return an empty object, simply call complete()
. If you need to pass an empty array, you must call setBody()
with the empty Array
.
// Sets the response to an empty objectS
complete();
// Sets the response to an empty array
complete().setBody([]);
For errors, you can either pass a string as the error message or a JavaScript Error
object.
// Sets the response to an error string to be used with error status functions
complete().setBody('Record 123 was not found');
// Sets the response to an Error object to be used with error status functions
complete().setBody(new Error('Record 123 was not found');
Status functions set the valid status code for a Flex function operation. The status function also sets the body to a Kinvey-formatted error and uses the value passed to the status function or the setBody()
function as the debug property, if it is present.
See the Flex API Reference for the available status functions.
For example:
// Return that the record has been created
complete().setBody(myRecord).created();
// Entity wasn't found
complete().setBody('The given entity wasn\'t found').notFound();
After the status is set, you can end the processing of the handler request with either done()
or next()
. Most requests should normally end with next()
, which continues the Kinvey request pipeline. The done()
function returns the response set in the completion handler and ends the request processing without executing any further functions.
// Continue the request chain
complete().setBody(myEntity).ok().next();
// End the request chain with no further processing
complete().ok().done();
Example
The following code provides a more complete example of how to structure a FlexFunctions service.
const sdk = require('kinvey-flex-sdk');
const request = require('request'); // Assumes that the request module
// was added to package.json.
sdk.service(function(err, flex) {
const flexFunctions = flex.functions; // Gets the FlexFunctions object
// from the service.
function getRedLineSchedule(context, complete, modules) {
request.get(
'http://developer.mbta.com/Data/Red.json',
(err, response, body) => {
// if error, return an error
if (err) {
return complete().setBody('Could not complete request')
.runtimeError().done();
}
//otherwise, return the results
return complete().setBody(body).ok().done();
});
}
// set the handler
flexFunctions.register('getRedLineData', getRedLineSchedule);
};
FlexAuth
The FlexAuth framework is used to execute custom authentication handlers for logging in through Mobile Identity Connect.
Initializing FlexAuth
You can access the FlexAuth framework through the Flex SDK's auth
property.
const auth = flex.auth;
Registering FlexAuth Handlers
To register a FlexFunction handler, you define that handler and give it a name by using the register()
method of the auth
object.
The following code snippet registers the someEventHandlerName
handler:
// To register the 'someEventHandlerName' handler
const mySSO = flex.auth.register('mySSO', eventHandlerCallbackFunction);
In the Kinvey Console, when you define a FlexAuth service, you will be presented with the list of authentication event handler names you've registered.
Handler Function
Each FlexAuth handler takes a handler functions with three arguments: context
, complete
, and modules
. The context
argument represents the state of the current Kinvey request context and complete
is a completion handler for completing the function. The modules
argument is an object containing several libraries for accessing Kinvey functionality through your service (for more information, see the section on modules).
complete()
method. This functionality is deprecated and will be removed in a future version of the SDK.
Accessing the Context
The context
object represents the state of the respective stage of the Kinvey request pipeline. Some of the object properties are set only at the appropriate stages.
See the Flex API Reference for full description.
Completing the Function
The completion handler initiates the complete process. It follows a builder pattern for creating the handler's response. The pattern for the completion handler is complete().[setToken(<token>).addAttribute(<key>, <value>).removeAttribute(<key>).<status>.done()|next()]
.
An example completion handler is:
complete().setToken(myToken).addAttribute('userEmail', myEmail).ok().next()
It provides a set of methods for altering the context of the request and a series of status functions.
FlexAuth supports the following context-altering methods:
Method | Description |
---|---|
setToken(token) | Sets the authentication token to be used for this user. |
addAttribute(key, value) | Adds custom attributes to your FlexAuth response. Optional. |
removeAttribtue(key) | Removes a previously added custom attribute from your FlexAuth response. Optional. |
Note that you must set a token explicitly using setToken()
. The value must be a JSON object or a Base64-encoded string.
// Sets the context to include an entity and an altered query
complete().setToken({'myAuthToken': 'ffds9afdsafdsaf89ds0fds90f8ds-='})
.addAttribute('email', 'test123@test.com');
For errors, you can either pass a string as the error message or a JavaScript Error
object.
// Sets the response to an error string to be used with error status functions
complete().setToken('Record 123 was not found');
// Sets the response to an Error object to be used with error status functions
complete().setToken(new Error('Record 123 was not found');
Status functions set the valid status code for a FlexAuth operation. The status function also sets the body to an OAuth-formatted error and uses the value passed to the status function as the debug property, if it is present.
See the Flex API Reference for the available status functions.
For example:
// Return that the user has been authenticated
complete().setToken(myToken).ok();
After the status is set, you can end the processing of the handler request with either done()
or next()
. Most requests should normally end with next()
, which continues the Kinvey request pipeline. The done()
function returns the response set in the completion handler and ends the request processing without executing any further functions.
// Continue the request chain
complete().setToken(myToken).ok().next();
// End the request chain with no further processing
complete().ok().done();
Example
The following code provides a more complete example of how to structure a FlexAuth service.
const sdk = require('kinvey-flex-sdk');
const request = require('request'); // Assumes that the request module
// was added to package.json.
sdk.service((err, flex) => {
const flexAuth = flex.auth; // Gets the FlexAuth object
// from the service.
function authenticate(context, complete, modules) {
// authenticate the user here
if (err) {
return complete().accessDenied(err).next();
}
return complete().setToken(token).ok().next();
}
// set the handler
flexAuth.register('myAuth', authenticate);
};
Flex Routes
Kinvey exposes a set of HTTP endpoints, called Flex routes, for each FlexService that you create. These endpoints are your FlexService's façade visible to apps that connect to your backend.
There is a separate set of Flex routes for FlexData, FlexFunctions, and FlexAuth, as well as a route for service discovery.
You need to implement an event handler for each route in your FlexService.
Routes for FlexData
The FlexData routes allow you to implement CRUD and count operations. See the Flex API Reference for a list of FlexData routes.
Routes for FlexFunctions
FlexFunctions exposes a route for each FlexFunctions function you create. The route name depends on the name you have given the function. See the Flex API Reference for details.
Routes for FlexAuth
FlexAuth exposes a route for each FlexAuth function you create. The route name depends on the name you have given the function. See the Flex API Reference for details.
Routes for Service Discovery
The service discovery route is an auxiliary route that returns all registered Flex objects on your Kinvey account. This includes FlexData service objects, FlexFunctions, and FlexAuth endpoints.
See the Flex API Reference for details.
Executing a Long-running Script
Because the services are persisted, you can execute long running tasks that run in the background. However, all Kinvey requests still need to send a response within 60 seconds. For internal Flex services, the FlexService Runtime sends an automatic timeout error even earlier.
To accomplish this, you can execute a function asynchronously using one of the Timer
functions from Node.js: setImmediate
, setTimeout
, or setInterval
.
Those interruptions are infrequent, but you must not rely on your code running forever. You should always write your background code in a way that tolerates interruptions.
For example:
const sdk = require('kinvey-flex-sdk');
const request = require('request'); // Assumes that the request module
// was added to package.json.
sdk.service(function(err, flex) {
const flexFunctions = flex.functions; // Gets the FlexFunctions object
// from the service.
function calcSomeData() {
// do something
}
function calcAndPostData() {
auth = {
user: '<MY_APP_KEY>',
pass: '<MY_APP_SECRET>'
};
options = {
url: 'https://baas.kinvey.com/appdata/<MY_APP_KEY?/someCollection',
auth: auth,
json: {
someData: calcSomeData(),
date: new Date().toString()
}
}
request.get(options, (err, response, body) => {
// if error, return an error
if (err) {
return console.log('Error: ' + err);
}
//otherwise, just return
return;
});
}
function initiateCalcAndPost(context, complete, modules) {
// Since the calc and post data function may take
// a long time, execute it asynchronously.
setImmediate(calcAndPostData);
// Immediately complete the handler function. The response will be returned
// to the caller and calcAndPostData will execute in the background.
complete().accepted().done();
// set the handler to point to the initiateCalcAndPost header
flexFunctions.register('getRedLineData', initiateCalcAndPost);
};
In the above example, the handler receives a request and executes the initiateCalcAndPost
function. The function schedules an immediate asynchronous execution of calcAndPostData
, and then executes the complete handler, returning control to the client and sending the response (in this case, accepted
, because the request is accepted for processing). The service stays running in the background, and the long-running calcAndPostData
function is executed asynchronously.
setImmediate
, setInterval
, and setTimeout
do not get the request metadata or the execution context automatically. If the request metadata is required during your code execution, for example for logging or debugging, you must explicitly pass this data to the callback function.
Modules
Modules are a set of libraries intended for accessing Kinvey-specific functionality. The optional modules
argument is passed as the third argument to all handler functions. For example:
// data handler
function onGetById(context, complete, modules) {
const appKey = modules.backendContext.getAppKey();
// etc...
}
You can use any non-Kinvey library or module by including it in your package.json
as you would for a standard Node.js project. The Flex SDK provides the following Kinvey-related modules:
- backendContext Provides methods to access information about the current backend context.
- dataStore Fetch, query, and write to Kinvey collections.
- email Send Email notifications
- groupStore Fetch, query, and write to Kinvey groups.
- Kinvey Entity Kinvey entity utilities
- kinveyDate Kinvey date utilities
- push Send push notifications to a user's device
- Query Create queries to be used by the dataStore.
- requestContext Provides methods to access information about the current request context.
- tempObjectStore Key-value store for persisting temporary data between a pre- and post-hook.
- userStore Fetch, query, create, update, delete, suspend and restore Kinvey Users.
- roleStore Fetches, queries, and writes Kinvey user roles. Lists role members.
- endpointRunner Calls custom endpoints with reusable user context and authentication.
Click each link to view the API reference information for the respective module.
Logging
In addition to the native JavaScript logging facilities like console.log()
, the Flex SDK offers a set of its own logging functions. Using these functions may be beneficial when running an external Flex service as they always persist log messages on the server.
After you've collected some logs, you can use Kinvey CLI to read and filter through them. See the flex log
subcommand for details.
Retention Policy
Flex.logger log entries are stored on the Kinvey servers and are subject to some restriction as described bellow.
- The number of log messages is capped at 500,000 entries per Flex service.
- The cumulative size of all messages cannot exceed 100 MB per Flex service.
If any of the restrictions is reached, the system starts deleting the oldest entries to make room for new entries.
stdout
.
Logging Messages
The logger
module provides a different logging function for each severity level: info()
, warn()
, error()
, fatal()
. Each of them takes a string argument representing the log message. You can also pass an instance of Error
directly and it will be automatically stringified.
The following example defines a function that deletes entities older than a specified number of days. It reads the number of days from the request body which is accessible through the context. The logger
functions are used three times: to log an info
-level message indicating each function execution, an error
-level message containing the stingified error in case of failure, and another info
-level message with the response body in case of success.
function deleteOldItems(context, complete, modules) {
const logger = flex.logger;
logger.info('Executing deleteOldItems Flex function');
const deleteBefore = context.body.deleteBefore;
const query = new modules.Query();
query.lessThanOrEqualTo('_kmd.ect', deleteBefore);
const bookStore = modules.dataStore().collection("Books");
bookStore.remove(query, function(err, count) {
if (err) {
logger.error('Delete failed: ' + JSON.stringify(err));
complete().setBody(err).runtimeError().done();
} else {
const response = {'Deleted records': count};
logger.info('Deletion succeeded: ' + JSON.stringify(response.body));
complete().setBody(response).ok().done();
}
});
}