IMFV
Objective
SOAJS is equipped with an input mapping, filtering and validating mechanism that removes up to 50% of an API code in terms of fetching and cleaning the inputs from POST, GET, SESSION, COOKIES, HEADERS and TENANT CONFIG.
Every service uses the IMFV (input mapping, filtering and validating) to validate the inputs (parameters) of its APIs. IMFV is usually located in a config.js file within the service directory.
This space will show you:
- Where the IMFV is defined
- Error codes
- Sample Schema
- Sources List
- Default Values
- Types, Properties, and Formats
- Type Casting
- Complex Schemas
- Common Fields
- API Label Info
- Usage
- Full Sample
Config.js
The content of this file is based on two main sections: Error Codes ("errors" object) and Validation Schema ("schema" object).
//inside config.js module.exports={ "serviceName": "helloworld", "servicePort": 4020, "extKeyRequired": false, "errors": {}, "schema": {} }
Error Codes
Error Codes is a list that contains all the errors that might be returned by the service APIs. Each Error consists of a number referred to by code and an error message.
"errors":{ //... 401: "unable to login, user not found!", 402: "Invalid username provided!" //... }
When an error in the service occurs, the service will return back to the user a code and a message for clarification. To use the error codes inside the service code index.js, use req.soajs.buildResponse and provide the error object as the first parameter. The error object is constructed from the code and the message.
//inside index.js var config = require("./config"); var service = new soajs.server.service(...); service.post("/login", function(req, res) { //... if(req.soajs.inputmaskData.username === ''){ return res.jsonp(req.soajs.buildResponse({"code": 402, "msg": config.errors[402]})); } //... };
SOAJS built-in req.soajs.buildResponse takes 2 parameters, the first being an error object and the second being the response.
Sample Schema
Every API has an entry inside the schema object; this is where you configure the inputs that it should receive and their validation.
Every entry instructs IMFV where to look for the input using the source attribute and how to validate the input using the validation attribute.
var config = { "serviceName": "helloworld", "servicePort": 4020, "extKeyRequired": false, "schema": { //api route "/hello": { "_apiInfo":{ "l": "Hello Api" }, 'firstName': { 'source': ['query.firstName'], 'required': true, 'validation': { 'type': 'string', 'format': 'alphanumeric' //... } } //... } //... } };
The above sample Tells IMFV that for API "/hello" there is a required input called "firstName". It's type is string and it has a specific alphanumeric format and it is provided via the query string as "firstName" when the request is made.
Sources List
IMFV can fetch an input from different sources if configured to do so. In the case above, the "source" array has only one entry which is "query.firstName". However, you can configure IMFV to look in multiple sources for the input. If the first source doesn't contain the input value, IMFV will jump to the next one and so on till the entire "source" array has been processed.
"firstName" : { "source": ["query.firstName", "body.firstName", "session.firstName", ...] }
IMFV will look for "firstName" in the following sources, in this order: query string, then posted body, then session...
Default Values
IMFV offers the ability to provide a default value for an input. This means that if the input is not provided, IMFV will use the default value configured for that input.
//... "firstName": { "source": ['query.firstName'], "required": true, "default": "John", "validation": { "type": "string" } }, //...
The above code sets the value of firstName to "John". If the request is made to the API without specifying a firstName in the querystring, then req.soajs.inputmaskData.firstName will be set to John.
Types, Properties & Formats
IMFV supports different input types with different properties such as strings, numbers, boolean, arrays, objects, regular expressions... etc. Each of these types has different combination of properties. A string for example can represent an email, a name, a date or a phone number. Here is a list of what can be used with IMFV:
Boolean: either true or false.
"married": { "source": ['query.married'], "required": true, "default": false, "validation": { "type": "boolean" } }
regexp: represents a regular expression. Ex: /^\/admin\/.+/$
"route": { "source": ['body.route'], "required": true, "validation": { "type": "regexp" } }
number: represents both integers and floats.
"price": { "source": ['body.route'], "required": true, "validation": { "type": "number", "minimum": 0, //optional "maximum": 15.4 //optional } }
object: this input can hold different properties from all mentioned types.
"profile":{ "source": ['body.profile'], "validation":{ "type":"object", "properties":{ "age": { "type": "number", "minimum": 18 }, "gender": {"type":"string", "enum": ["male","female"]}, "images": {"type":"string", "format":"uri"}, "additionalProperties": false } } }
When the type is "array", define what is the type of the items in the array under "items".
array: this input can hold different items from all mentioned types, each item can have any type mentioned.
"users":{ "source": ['body.users'], "validation":{ "type":"array", "items":{ "type":"object", "properties":{ "name": { "type": "string", "minLength": 6 }, "age": { "type": "number", "minimum": 18 }, "gender": {"type":"string", "enum": ["male","female"]}, "images": {"type":"string", "format":"uri"}, "additionalProperties": false } }, "minItems": 1, //optional "maxItems": 10000, //optional "uniqeItems": true, //optional "additionalItems": false //optional } }
string: can have multiple formats applied to it.
"username":{ "source": ['body.username'], "validation":{ "type":"string", "minLength": 10, //optional "maxLength": 20, //optional "format": "alphanumeric" //optional } }
When dealing with strings, keep in mind that a string can represent several types of inputs and thus this type is commonly used when providing an api with parameters. Therefore a format option has been added to validate the content of a string and determine if the value resembles the expected outcome. Here is a list of the provided formats that can be used:
Format | Sample |
---|---|
date: | 0000-00-00 |
time: | 00:00:00 |
date-time: | 0000-00-00T00:00:00.000Z |
route: | /service/api/pathparam |
phone: | +(000)-000-0000 |
email: | me@domain.com |
ip-address: | 127.0.0.1 |
ipv6: | FE80:0000:0000:0000:0202:B3FF:FE1E:8329 |
uri: | http://someurl.com/somepath/somenode |
domain: | someurl.com |
hostname: | someurl |
alpha: | John |
alphanumeric: | John179 |
color: | blue |
style: | background-color:red |
Type Casting
IMFV supports all inputs provided if the request headers states that it hold a "content-type" of value "application/json". This means that the arriving input is a JSON input and do not need to apply type casting on them. Any request that does not contain this header, will provide the inputs as strings and therefore these inputs need to be parsed before validation otherwise they will all fail.
Complex Schemas
IMFV supports complex schemas such as arrays and objects, where each of the two contains basic or even more complex schemas internally. It relies on JSON Schema’s operator such as oneOf, anyOf, allOf in the cases where the same input might have different values.
IMFV also uses additionalProperties and patternProperties, from JSON Schema, within object schemas giving you validation control on optional api fields and complex input names such as api routes.
"use strict"; var accessSchema = { "oneOf": [ {"type": "boolean", "required": false}, {"type": "array", "minItems": 1, "items": {"type": "string", "required": true}, "required": false} ] }; module.exports = { 'acl': { 'source': ['body.acl'], 'required': false, 'validation': { "type": "object", "additionalProperties": { "type": "object", "required": false, "properties": { "access": accessSchema, "apisPermission": { "type": "string", "enum": ["restricted"], "required": false }, "apis": { "type": "object", "required": false, "patternProperties": { //pattern to match an api route "^[_a-z\/][_a-zA-Z0-9\/:]*$": { "type": "object", "required": true, "properties": { "access": accessSchema }, "additionalProperties": false } } } }, "additionalProperties": false } } } };
Common Fields
commonfields is used in IMFV to group inputs that have similar mapping and validation. Consider you have 2 or more APIs that use the same input "firstName" and this input is read and validate the same way across those APIs. To avoid rewriting the configuration and make room for errors, add the input under commonFields then in every API configuration, reference its presence.
//... "schema": { "commonFields": { //add firstName to commonFields "firstName": { "source": ['query.firstName'], "required": true, "default": "John", "validation": { "type": "string" } } }, "/api_route1": { "_apiInfo": { ... }, "commonFields": ['firstName'], //reference firstName from common fields "lastName" : { ... } }, "/api_route2":{ "_apiInfo": { ... }, "commonFields": ['firstName'], //reference firstName from common fields "email" : { ... } } } //...
API Label Information
Every API contains a property called _apiInfo which is used in by the dashboard UI when accessing the service information page.
Property Name | Property Type | Description | Mandatory |
---|---|---|---|
l | String | Contains the API Label | YES |
group | String | Contains the API Group Name | NO |
groupMain | boolean | States if this API is the Group default API or not. | NO |
Based on the above table, the API information can be provided in three different schemas as follow:
# containing only label '/users/list':{ '_apiInfo': { 'l': 'List Users' } .... } # containing a label and a group '/users/list':{ '_apiInfo': { 'l': 'List Users', 'group': 'Users Management' } .... } # containing a label, a group and set to Default '/users/list':{ '_apiInfo': { 'l': 'List Users', 'group': 'Users Management', 'groupMain': true } .... } '/users/add':{ '_apiInfo': { 'l': 'Add New User', 'group': 'Users Management' } .... }
Usage
IMFV consolidates all inputs after validation inside req.soajs.inputmaskData making it easier for developers to use them. Instead of wondering if the input was mapped correctly from the request, simply use req.soajs.inputmaskData[input_name] to retrieve the input value and use it in the code.
//inside index.js var config = require("./config"); var service = new soajs.server.service(config); service.post("/login", function(req, res) { //... if(req.soajs.inputmaskData.username === ''){ return res.jsonp(req.soajs.buildResponse({"code": 402, "msg": config.errors[402]})); } //...
As illustrated in the above code, regardless of the source that provided it, username input can now be accessed from req.soajs.inputmaskData object. The same applies to all other inputs passed to the requested API.
Full Sample
The following is a snippet of both config.js and index.js used together to implement a service that is built with SOAJS; it focuses on how to use the errors and IMFV schema validation.
'use strict'; var soajs = require('soajs'); var config = require('./config.js'); var service = new soajs.server.service(config); service.init(function(){ service.get("/testGet", function(req, res) { //do some business logic here ... res.json(req.soajs.buildResponse(null, { id: req.soajs.inputmaskData.id, firstName: req.soajs.inputmaskData.firstName })); }); service.post("/testPost", function(req, res) { //do some business logic here ... res.json(req.soajs.buildResponse(null, { id: req.soajs.inputmaskData.id, email: req.soajs.inputmaskData.email, _TTL: req.soajs.inputmaskData._TTL, acl: req.soajs.inputmaskData.acl })); }); service.start(); });
'use strict'; var accessSchema = { "oneOf": [ {"type": "boolean", "required": false}, {"type": "array", "minItems": 1, "items": {"type": "string", "required": true}, "required": false} ] }; module.exports = { "serviceName": "example01", //... "someVariable": "somveValue", //... "errors": { "101": "some error message...", "102": "some error message..." //... }, "schema": { "commonFields": { 'id': { 'source': ['query.id', 'body.id', 'session.userId'], 'required': true, 'validation': { 'type': 'string', "minLength": 24, format: 'alphanumeric' } }, //common field acl 'acl': { 'source': ['body.acl'], 'required': false, 'validation': { "type": "object", "additionalProperties": { "type": "object", "required": false, "properties": { "access": accessSchema, "apisPermission": { "type": "string", "enum": ["restricted"], "required": false }, "apis": { "type": "object", "required": false, "patternProperties": { //pattern to match an api route "^[_a-z\/][_a-zA-Z0-9\/:]*$": { "type": "object", "required": true, "properties": { "access": accessSchema }, "additionalProperties": false } } } }, "additionalProperties": false } } } }, //api route testGet "/testGet": { "_apiInfo": { "l": "Test Get API" }, 'commonFields': ['id'], 'firstName': { 'source': ['query.firstName'], 'required': true, 'validation': {'type': 'string', format: 'alphanumeric'} } }, //api route testPost "/testPost": { "_apiInfo": { "l": "Test Post API" }, "commonFields": ['id', 'acl'], "email": { "source": ["body.email"], "required": true, 'validation': {'type': 'string', format: 'email'} }, "_TTL": { "source": ["session.user._TTL", "tenantConfig._TTL"], "required": true, 'validation': {'type': 'number', "enum": [6, 12, 24, 48]}, 'default': 6 } } //... } };