@omneo/cf

Omneo CF helps you develop cloud function plugins for Omneo

Usage no npm install needed!

<script type="module">
  import omneoCf from 'https://cdn.skypack.dev/@omneo/cf';
</script>

README

Omneo Cloud Function Helpers

This is a small library to help boilerplate cloud functions for Omneo. It includes a bunch of handy helpers to help you get started and internal Express handlers for your routes and controllers.

Getting started

Install with yarn or npm

yarn add @omneo/cf
# OR
npm install @omneo/cf

Require @omneo/cf then call the returned function with an object containing top level route path and controller. To make things simple, OmneoCF will register your controllers with the Express.js use() call.

const app = require('@omneo/cf');

const profileController = (req, res) => {
    res.status(200).json({
        message: `You made a ${req.method} request to the profile controller`
    })   
}

const transactionController = () => {
    const router = express.Router();
    router.get('/', (req, res)=>{
        res.status(200).json({
            message: "Transactions"
        })  
    })

    router.post('/items', (req, res)=>{
        res.status(200).json({
            message: "Transaction items"
        })    
    })
}

return app({
    profiles: profileController,
    transactions: transactionController()
})

Automatic environment handling

This library looks at NODE_ENV and will either export just the routes or will start a local express server. The local server will run on NODE_PORT || 3000

This local server will also apply the same bodyParser as GCP, to give you access to both res.body and res.rawBody - Useful when validating webhooks etc. It also uses dotenv() to include your .env file in process.env.

Batteries-included middleware

The basics

Both local and production outputs include cors() and compression() Express libraries and a basic request logger. Logging can be disabled by setting the following environment variable: LOGGING=disabled

Error handling

All routes are wrapped in an error handler to ensure the service returns an error http response if possible. Note: You should still wrap your controllers in a try/catch to ensure the best handling of this.

Tenant validation

All routes registered with the main function will sit under a /:tenant route. This is used by the middleware/validateOmneoTenant function to check for tenant configuration in process.env. By default, only [tenant]_omneo_token and [tenant]_omneo_secret should be stored in env - All other configurations should be stored elsewhere, such as Omneo Tenant Fields or a DB.

This middleware also adds the omneo_token and omneo_secret to globals.tenant, along with omneo_url, matching the following format: [tenant].getomneo.com

Omneo Plugin validation

All routes will be valiadated and make sure plugin has been configured in omneo. It's handled by middleware/validateOmneoPluginConfig. Plugin handle needs to match the PluginNamespace and "Enabled" field is required and needs to be true for successful validation. This requirement is valid for version >= 1.0.0.

Webhook validation

This library includes middleware for Omneo webhook validation at middleware/validateOmneoWebhook This middleware accepts an array of allowed events and will automatically check against the headers Omneo uses for webhook HMAC signatures and event.

In addition, an abstracted middleware/validateWebhook is availble for services that use similar methods for webhook validation:

validateWebhook(
    events, // Array of allowed event strings eg. ['profile.created']
    headerSignature, // Header key for the HMAC signature eg. 'x-omneo-hmac-sha256'
    headerEvent, // Header key for the webhook event eg. 'x-omneo-event'
    hmacSecret, // Secret key used to decode. For Omneo we use the OMNEO_SECRET
    alg // HMAC algorithm to use when decoding
)

A handful of handy utilities

To help make your development life easier, this library includes a small set of useful utility functions.

Fetch

Our own wrapper for the popular node-fetch library. This includes a number of helpers to construct your requsts, as well as response parsing, so you don't have to separately run response.json().

Note that config.global is only available after the validateOmneoTenant middleware has run, not at initialization. This means that any function relying on these will need to be initialized after that.

// omneo.js
const {Fetch} = require('@omneo/cf/utilities');

export.default = () => new Fetch(`https://api.${global.config.omneo_url}/api/v3`, {
    headers: {
        Authorization: `Bearer ${global.config.omneo_token}`
    }
})
// route.js
const omneoClient = require('./omneo');

const route = async (req, res) => {
    const omneo = omneoClient();
    // Searching profiles. get(url, params)  
    const search = await omneo.get('/profiles', {
        "filter[email]":"richard@piedpiper.com"
    })

    // Creating a profile: post(url, body)
    const create = await omneo.post('/profiles', {
        first_name: "Gavin",
        last_name: "Belson",
        email: "gavin@hooli.com"
    })

    res.send()
}

Pluck

Dives through a js/json object, by key, to find the value. Returns the value if found, or null where not available. Uses memoizee to memoize the value for subsequent attempts.

const {pluck} = require('@omneo/cf/utilities');
const data = {
    "some":{
        "nested":{
            "value":"Success value"
        }
    }
}

pluck(data, 'some.nested.value') // Returns "Success value"
pluck(data, 'some.other.value') // Returns null"

Pluck By

Iterates through an array of objects, to find matching key:value pair and return that object. Can take a 3rd argument to find the array in an object first, using pluck;

const {pluckBy} = require('@omneo/cf/utilities');
const {
    data: {
        profile: {
            "first_name":"Erlich",
            "last_name":"Bachman",
            "email":"erlich@bachmanity.com",
            "attributes":{...},
            "identities":[
                {
                    "handle":"shopify",
                    "identifier":"SHOP12345"
                },
                {
                    "handle":"zendesk",
                    "identifier":"ZEN67890"
                }
            ]
        }
    }
}

pluckBy(data.profile.identities, 'identifier', 'ZEN67890') // Returns {"handle":"zendesk","identifier":"ZEN67890"}
pluckBy(data.profile.identities, 'identifier', 'ZEN12345') // Returns null
pluckBy(profile, 'handle', 'shopify', 'profile.identities') // Returns {"handle":"shopify","identifier":"SHOP12345"}

ErrorHandler

Error constructor used to append additional details to the handleErrors middleware. Takes status, message and a json payload

const route = require('express').Router();
const {ErrorHandler} = require('@omneo/cf/utilities');

route.post('/', (req, res, next)=>{
    const profile = req.body
    try{
        if(!profile.email){
            throw new ErrorHandler(400, "Profile is incomplete", {
                errors:{
                    email: "This field is required"
                }
            })
        }
        return profile
    }catch(e){
        next(e);
    }
})

module.exports = route