0.0.3 • Published 4 years ago

@externs/idio v0.0.3

Weekly downloads
4
License
AGPL-3.0
Repository
github
Last release
4 years ago

@idio/idio

npm version

@idio/idio contains Koa's fork called Goa web server compiled with Closure Compiler so that its source code is optimised and contains only 1 external dependency (mime-db). Idio adds essential middleware to Goa for session, static files, CORS and compression and includes the router. As the project grows, more middleware will be added and optimised.

This is a production-ready server that puts all components together for the ease of use, while providing great developer experience using JSDoc annotations for auto-completions. Idio is not a framework, but a library that enables idiomatic usage and compilation of the server and middleware.

yarn add @idio/idio

Table Of Contents

API

The package is available by importing its default function and named components:

import idio, { Keygrip } from '@idio/idio'

async idio(  middlewareConfig=: !MiddlewareConfig,  config=: !Config,): !Idio

Start the server. Sets the proxy property to true when the NODEENV is equal to _production.

  • middlewareConfig !MiddlewareConfig (optional): The middleware configuration for the idio server.
  • config !Config (optional): The server configuration object.

The app can be stopped with an async .destroy method implemented on it that closes all connections.

There are multiple items for middleware configuration:

MiddlewareConfig extends FnMiddlewareConfig: Middleware configuration for the idio server.

NameTypeDescription
staticStaticOptionskoa-static options.
compressCompressOptionskoa-compress options.
sessionSessionOptionskoa-session options.
corsCorsOptionskoa-cors options.

The types for starting the server include the address, port and router configuration.

Config: Server configuration object.

NameTypeDescriptionDefault
portnumberThe port on which to start the server.5000
hoststringThe host on which to listen.0.0.0.0
router!_goa.RouterConfigThe configuration for the router.-

After the app is started, it can be accessed from the return type.

Idio: The return type of the idio.

NameTypeDescription
url*stringThe URL on which the server was started, such as http://localhost:5000.
server*!http.ServerThe server instance.
app*!ApplicationThe Goa application instance (with additional .destroy method).
middleware*!Object<string, !Middleware>An object with configured middleware functions, which can be installed manually using app.use, or router.use. The context will be a standard Goa context with certain properties set by bundled middleware such as .session.
router*!_goa.RouterThe router instance.

The example below starts a simple server with session and custom middleware, which is installed (used) automatically because it's defined as a function.

const { url, app } = await idio({
  // Idio's bundled middleware.
  session: {
    use: true,
    keys: new Keygrip(['hello', 'world']),
    config: {
      prefix: 'example-',
    },
  },

  // Any middleware function to be installed.
  async middleware(ctx, next) {
    ctx.body = 'hello world'
    await next()
  },
})
http://localhost:5000
hello world

Static

const { url, app } = await idio({
  static: {
    root: 'example', use: true,
  },
// or multiple locations
  static: [{
    root: ['example'], use: true,
  }, {
    root: ['d'], use: true,
  }],
})
/** http://localhost:53685/app.css */ 

body {
  font-size: larger;
}
<!-- http://localhost:5000/em.svg --> 

<xml></xml>
Content-Length: 29
Last-Modified: Thu, 18 Jul 2019 14:34:31 GMT
Cache-Control: max-age=0
Content-Type: text/css; charset=utf-8
Date: Fri, 20 Dec 2019 07:04:57 GMT
Connection: close
Content-Length: 11
Last-Modified: Thu, 18 Jul 2019 14:47:20 GMT
Cache-Control: max-age=0
Content-Type: image/svg+xml
Date: Fri, 20 Dec 2019 07:05:38 GMT
Connection: close

Session

👳‍♂️Explore Session Middleware Configuration

Allows to store data in the .session property of the context. The session is serialised and placed in cookies. When the request contains the cookie, the session will be restored and validated (if signed) against the key.

const { url, app } = await idio({
  session: { use: true, keys:
    ['hello', 'world'], config: {
    signed: false,
  } },
  async middleware(ctx, next) {
    if (ctx.session.user)
      ctx.body = 'welcome back '
        + ctx.session.user
    else {
      ctx.session.user = 'u'
        +( Math.random() * 1000).toFixed(1)
      ctx.body = 'hello new user'
    }
    await next()
  },
})
http://localhost:5000 

/ hello new user
/ welcome back u190.1

Custom Middleware

When required to add any other middleware in the application not included in the Idio bundle, it can be done in several ways.

  1. Passing the middleware function as part of the MiddlewareConfig. It will be automatically installed to be used by the Application. All middleware will be installed in order it is found in the MiddlewareConfig.

    import idio from '@idio/idio'
    
    const APIServer = async (port) => {
      const { url } = await idio({
        // 1. Add logging middleware.
        async log(ctx, next) {
          await next()
          console.log(' --> API: %s %s %s', ctx.method, ctx.url, ctx.status)
        },
        // 2. Add always used error middleware.
        async error(ctx, next) {
          try {
            await next()
          } catch (err) {
            ctx.status = 403
            ctx.body = err.message
          }
        },
        // 3. Add validation middleware.
        async validateKey(ctx, next) {
          if (ctx.query.key !== 'app-secret')
            throw new Error('Wrong API key.')
          ctx.body = 'ok'
          await next()
        },
      }, { port })
      return url
    }
    
    export default APIServer
    Started API server at: http://localhost:5005
     --> API: GET / 403
     --> API: GET /?key=app-secret 200
  2. Passing a configuration object as part of the MiddlewareConfig that includes the middlewareConstructor property which will receive the reference to the app. Other properties such as conf and use will be used in the same way as when setting up bundled middleware: setting use to true will result in the middleware being used for every request, and the config will be passed to the constructor.

    import rqt from 'rqt'
    import idio from '@idio/idio'
    import APIServer from './api-server'
    
    const ProxyServer = async (port) => {
      // 1. Start the API server.
      const API = await APIServer(5001)
      console.log('API server started at %s', API)
    
      // 2. Start a proxy server to the API.
      const { url } = await idio({
        async log(ctx, next) {
          await next()
          console.log(' --> Proxy: %s %s %s', ctx.method, ctx.url, ctx.status)
        },
        api: {
          use: true,
          async middlewareConstructor(app, config) {
            // e.g., read from a virtual network
            app.context.SECRET = await Promise.resolve('app-secret')
    
            /** @type {import('@typedefs/goa').Middleware} */
            const fn = async (ctx, next) => {
              const { path } = ctx
              const res = await rqt(`${config.API}${path}?key=${ctx.SECRET}`)
              ctx.body = res
              await next()
            }
            return fn
          },
          config: {
            API,
          },
        },
      }, { port })
      return url
    }
    API server started at http://localhost:5001
    Proxy started at http://localhost:5002
     --> API: GET /?key=app-secret 200
     --> Proxy: GET / 200

Router Set-up

After the Application and Router instances are obtained after starting the server as the app and router properties of the returned object, the router can be configured to respond to custom paths. This can be done by assigning configured middleware from the map and standalone middleware, and calling the use method on the Application instance.

import { collect } from 'catchment'
import idio from '@idio/idio'

const Server = async () => {
  const {
    url, router, app, middleware: { pre, post, bodyparser },
  } = await idio({
    // 1. Configure middlewares via middlewareConstructor without installing them.
    pre: {
      middlewareConstructor() {
        return async function(ctx, next) {
          console.log('  <-- %s %s',
            ctx.request.method,
            ctx.request.path,
          )
          await next()
        }
      },
    },
    post: {
      middlewareConstructor() {
        return async function(ctx, next) {
          console.log('  --> %s %s %s',
            ctx.request.method,
            ctx.request.path,
            ctx.response.status,
          )
          await next()
        }
      },
    },
    bodyparser: {
      middlewareConstructor() {
        return async (ctx, next) => {
          let body = await collect(ctx.req)
          if (ctx.is('application/json')) {
            body = JSON.parse(body)
          }
          ctx.request.body = body
          await next()
        }
      },
    },
  }, { port: 5003 })

  // 2. Setup router with the bodyparser and path-specific middleware.
  router.post('/example',
    pre,
    bodyparser,
    async (ctx, next) => {
      ctx.body = {
        ok: true,
        request: ctx.request.body,
      }
      await next()
    },
    post,
  )
  app.use(router.routes())
  return url
}
Page available at: http://localhost:5003
  <-- POST /example
  --> POST /example 200
// server response:
{ ok: true, request: { hello: 'world' } }

Copyright & License

GNU Affero General Public License v3.0

All original work on middleware and Koa are under MIT license.