@pubcore/node-composition

composition of components served by express

Usage no npm install needed!

<script type="module">
  import pubcoreNodeComposition from 'https://cdn.skypack.dev/@pubcore/node-composition';
</script>

README

Express middleware to map requests to functions of components

Within the terminology of HTTP we do have requests and responses. Given a response is just a function of a request, this package provide the option to configure such functional mappings.

In order to structure such functions the words "component" and "composition" are used in this way:
A component is a set of functions.
A composition is a set of components.
A requested domain (on a specific port) is mapped to a composition.

The purpose of this package is to support such a structure by configuration.

Prerequisites

  • nodejs
  • expressjs

Auto invalidation of require-cache in development mode

Since nodejs caches required (imported) packages, changes within a file does not affect a running express server, it needs to be restared. This is expensive for continuously change-save-and-review cycles web-developers love.
This package implements automatic invalidation of require-cache per component-package level. If a script file changes, all modules of corresponding component-package gets invalidated.

Example composition

Let's assume we compose a todo-list component together with a calendar component. Composition's package directory consists of:

config.js
package.json
server.js

config.js (map request to components based on context-path)

//composition config
module.exports = {
    //a composition is a set of components ...
    components:{
        '@yourOrg/todo-list':{
            public: true,
            context_path: '/todo'
        },
        '@yourOrg/calendar':{
            public: true,
            context_path: '/calendar'
        }
    }
}

npm's package.json

{
    "name": "@yourScope/example-composition",
    "version": "1.0.0",
    "main": "server.js",
    "dependencies": {
        "express": "^4.17.1",
        "@pubcore/node-composition": "^2.8.0",
        "@yourScope/todo-list": "^0.1.0",
        "@yourScope/calendar": "^0.1.0",
    }
}

server.js

const
    express = require('express'),
    app = express(),
    composition = require('@pubcore/node-composition').default,
    config = require('./config.js')

app.use('/', composition(config, require))
Configuration options
module.exports = {
    componentDefault:{
        //if true, login (next option) is required
        public: false
        //login middleware, required, if component is not public
        login: (req, res, next) => {next()},

        //optional, build arbitrary data added to req.resources
        resources: async (req) => {}

        //optional error handler middleware
        error: (err, req, res, next) => {},

        //optional urlencoder middleware
        //see http://expressjs.com/de/api.html#express.urlencoded
        urlencoded: {extended: true}
    },
    components: {
        "@company/component-one":{
            //component ID
            id: "@company/component-one"

            //context path used for express Router
            context_path: "/basePathOfComponentOne"

            //optional to define (overwrite) defaults, see "componentDefault" ...
        }
    },
    accesscontrol:{
        //see https://developer.mozilla.org/en-US/docs/Glossary/CORS
        //CORS headers are responded for requests send from sites of following
        allowedOrigins: ["https://foo.net"],

        //see https://developer.mozilla.org/en-US/docs/Web/HTTP/CSP
        contentSecurityPolicy: "default-src 'self' data:; script-src 'self' font-src https:; style-src 'unsafe-inline' https:;"

        //Cross Site Request Forgery (SCRF) Protection by Double Submit Cookie Pattern
        //see https://github.com/expressjs/csurf
        //Secure config to store token-secret by cookie:
        csrfProtection: {key:'__Host-Csrf', secure:true, sameSite:'lax', httpOnly:true}
    },
    //optional
    options:{
        //optional
        requestJsonLimit: '2mb' //default 100kb
    }
}

Example component

A component package exports the mapping of URI sub-path to a express middleware function:

  1. src/index.js
//import express middleware functions
import list from './lib/getList'
import addItem from './lib/addItem'
export default {
    public:true,
    http: [
        {
            routePath: '/list',
            map: list,
            method: 'GET',
            accepted: ['text/plain']
        },
        {
            routePath: '/list',
            map: addItem,
            method: 'POST',
            accepted: ['application/json'],
            urlencoded: {extended: true} //optional, see above
        },
    ]
}
  1. Optional, public "htdocs" directory contain some static files (e.g. imgage, css, js)
    htdocs/

Features test output

compose components by configuration
    ✓ serves requests for some configured component functions
    ✓ serves requests for configured second "component-two"
    ✓ requires a login middleware function, if component is private
    ✓ reloads modules in development mode, if corresponding js file changed (131ms)
    ✓ reloads modules in development mode, if corresponding js file changed (113ms)
    ✓ supports CORS - CrossOriginResourceSharing by config (allowedOrigins)
    ✓ sends CSP - Content Security Policy header, if configured
    ✓ offers req.cookies and req.cookiesByArray object, if there are cookies

compose, if validation of a component fails
    ✓ should skip corresponding component and response with status 500


compose, if environment is in PRODUCTION mode
    ✓ does not load changed module’s script file and shows same result as before

component router
    ✓ routes requests based on component config
    ✓ support different methods for same path
    ✓ checks accept header
    ✓ checks http method
    ✓ checks http method before login
dev-mode: synchronous reload for @scope-a/component-one
dev-mode: synchronous reload for ./js/index
    ✓ responses "not found" for other paths
    ✓ requires authentication, if component or function is not public
    ✓ invokes a "login" promise for private resources
    ✓ removes passwort after login, for security reasons
    ✓ invokes a "resources" promise, if configured
    ✓ invokes a "resources" promise, if configured and use config data
    ✓ it catches up failed "resources" promise
    ✓ supports error handler middleware
    ✓ supports Content-Type: application/x-www-form-urlencoded
    ✓ supports to turn off "urlencoded" middleware endpoint specific
    Cross Site Request Forgery protection
        ✓ response a session-secret cookie, based on config)
        ✓ serves 403, if token is invalid
        ✓ accepts valid token send by form hidden field "_csrf"

References

CQRS protection "__Host-" cookie prefix