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:

  1. Where the IMFV is defined
  2. Error codes
  3. Sample Schema
  4. Sources List
  5. Default Values
  6. Types, Properties, and Formats
  7. Type Casting
  8. Complex Schemas
  9. Common Fields
  10. API Label Info
  11. Usage
  12. 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 stringsnumbersbooleanarraysobjectsregular 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:

FormatSample
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 oneOfanyOfallOf 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 NameProperty TypeDescriptionMandatory
lStringContains the API LabelYES
groupStringContains the API Group NameNO
groupMainbooleanStates 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.

Service Index
'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();
});
Service Config
'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
            }

        }
        //...
    }
};