@mazeltov/middleware

1.0.8 • Public • Published

Reusable Middleware

The repo contains re-usable middleware factories that can be used to simplify API building

Request Lifecycle

  • Values are copied with useArgs from query, params, and body into an object called req.args
  • Args are validated using validateArgs
  • Args are then consumed by a model using consumeArgs which:
    • Puts the successful result in res.locals.result
    • Or puts the caught error in res.locals.error
  • viewJSON, viewTemplate is used to render the result or error as JSON or HTML

Basic Example

This is the most barebones example usage to make it clear what is going on without access control.

const {
  useArgs,
  validateArgs,
  consumeArgs,
  viewJSON,
} = require('@mazeltov/middleware');

const {
  validate: {
    withLabel,
    isNotEmpty,
    isString,
    isOneOf,
  },
} = require('@mazeltov/util');

const router = require('express').Router()

const logger = require('@mazeltov/logger')('myApp/route/foo');

router.get('/foo/:id', [
  useArgs({
    params: ['id'],
    query: [
      // you can alias args (source -> alias)
      ['motive', 'fooMotive'],
      'isImportant'
    ],
    // static args are like defaults
    static: {
      activated: false,
    },
    // each middleware accepts a logger but the default is global console
    logger,
  }),
  validateArgs({
    // This is purely illustrative. You actually want to put validation in model
    // as validateGet, validateCreate and pass as validator key.
    validate: {
      fooMotive: withLabel('Foo motive', [
        isNotEmpty,
        isString,
        [isOneOf, ['love', 'honor']],
      ]),
    },
    logger,
  }),
  consumeArgs({
    // here you would replace with your model method like foo.get
    consumer: (args) => args,
    logger,
  }),
  viewJSON({ logger }),
]);

Real Usage

Here is a complete endpoint using authentication and authorization that produces JSON

const {
  requireAuth,
  canAccess,
  useArgs,
  validateArgs,
  consumeArgs,
  viewJSON,
} = require('@mazeltov/middleware');

const logger = require('@mazeltov/logger')('My logger label');

const router = require('express').Router();

const db = require('some-db');

const {
  personRoleModel,
} = require('@mazeltov/model')({ db }, [
  'personRole',
]);

// require your own model
const bookModel = /* ... */

router.get('/book/:id', [
  requireAuth({
    publicKeyPem: 'PUBLIC KEY PEM TEXT HERE',
    logger,
  }),
  canAccess({
    personRoleModel,
    checkMethod: bookModel.canGet,
    bypassClient: true,
    logger,
  }),
  useArgs({
    params: ['id'],
    logger,
  }),
  validateArgs({
    validate: {
      id: withLabel('ID', [
        isNotEmpty,
        isString,
      ]),
    },
    logger,
  }),
  consumeArgs({
   consumer:  bookModel.getBook
   logger,
  }),
  viewJSON(),
]);

Standard Response Patterns

Each of the examples below use the resource racoon as an example.

Global Rules:

  • Singular MUST be used! It is not GET /racoons, it is GET /racoon/list
  • All responses return an object with a result and error key.
  • All messages and errors should be human/customer readable
    • No programmer talk (error, exception, record, uncaught, boolean, array)
  • The following vernacular MUST be used in any client facing messages for each action
    • "remove" instead of "delete" or "destroy"
      • YES : The person you are trying to remove could not be found
      • NO: The person could not be deleted, Error cannot destroy person
    • "find" instead of "get" or "locate"
      • YES: The person could not be found
      • NO: Cannot get person
    • "update" is fine but should be supported with detailed list of field errors
    • "create" is fine but should be supported with detailed list of field errors

Result property: Object, Array, null

  • The result property is
    • An object for single resource records for GET, PUT, PATCH
    • An object for a summary of actions
      • numRemoved for DELETE endpoints
    • An array of objects for listing multiple records
    • An empty array when there are no records in a list
    • null otherwise (not found, conflict, gone)

Error property: Object

The error property is always extant and is an object. The lookup and list sub-properties are always an object and array respectively (even when empty).

  • A lookup with error keys that can be:
    • The machine readable name of form field that threw the error
    • Any one of these error keys to indicate a general error
      • _badRequest
      • _unauthorized
      • _forbidden
      • _notFound
      • _conflict
      • _gone
      • _unprocessableEntity
      • _serverError
      • _timeout
  • A list of errors ordered by key in ascending alphabetical order
    • General errors prefixed with underscores will then be hoisted on top
    • Must be an array of objects
    • Each object must have:
      • A key that matches what is in the lookup
      • A message
      • Optionally (but recommended if possible) an appropriate help link

GET /racoon/:id

Found (Status 200)

{
  result: { id: 12 },
  error: {
    lookup: {},
    list: {},
  },
}

Not Found (Status 404)

{
  "result": null,
  "error": {
    "lookup": {
      "_notFound": true,
    },
    "list": [
      {
        "key": "_notFound",
        "message": "The racoon you are looking for could not be found",
        "help" : "https://help.example.com/racoon/#get-a-racoon",
      }
    ],
  },
}

PUT /racoon/:id

Successful Update (Status 200)

This should return the new, updated record under result

{
  "result": {
    id: 12,
    name: 'New racoon name'
  },
  "error": {
    "lookup": {},
    "list": {},
  },
}

No Extant Record (Status 409 "Conflict")

When no record exists to be updated, a 409 should be thrown

{
  "result": null,
  "error": {
    "lookup": {
      "_conflict": true,
    },
    "list": {
      {
        "key" : "_conflict",
        "message": "The racoon you are trying to update could not be found",
        "help": "https://help.example.com/racoon/#update-a-racoon",
      },
    },
  },
}

Record Was Deleted (Status 410 "Gone")

Whenever possible to detect a record was deleted, a 410 should be sent back. This is usually the case if soft-deletes are used on the record, but otherwise a 409 is acceptable.

This should look like the example above, but replace the _conflict key with _gone.

GET /racoon/list

When getting one or more of a resource, the /list suffix is used in the path. The singular name of the entity is always used in the URI (racoon, not racoons!). Additionally:

  • Pagination MUST be supported and not bypassable via API.

The /*/list endpoints MUST accept:

  • A page query parameter defaulting to 0
  • A limit query parameter defaulting to 12

The /*/list endpoints MUST additionally provide:

  • currentPage which shows the page of the result
  • nextPage which is currentPage + 1, but can be null if on the last page
  • prevPage which is currentPage - 1, but can be null if on the first page
  • total showing the total number of records across all pages

Found Records (200)

{
  "result": [
    { id: 1, name: 'Rocky' },
    { id: 2, name: 'Rocky' },
    { id: 3, name: 'Rocky' },
    { id: 4, name: 'Rocky' },
    { id: 5, name: 'Rocky' },
    { id: 6, name: 'Rocky' },
    { id: 7, name: 'Rocky' },
    { id: 8, name: 'Rocky' },
    { id: 9, name: 'Rocky' },
    { id: 10, name: 'Rocky' },
    { id: 11, name: 'Rocky' },
    { id: 12, name: 'Rocky' },
  ],
  "currentPage": 0,
  "nextPage": 1,
  "prevPage": null,
  "total": 32,
  "error": {
    "lookup": {},
    "list": {},
  },
}

No Records (Status 200 "Found")

A 200 status code is still returned even when no records are found

204 and 206 are not used because the content is not considered missing or partial. The response is considered complete for our purposes.

{
  "result": [],
  "currentPage": 0,
  "nextPage": null,
  "prevPage": null,
  "total": 0,
  "error": {
    "lookup": {},
    "list": {},
  },
}

DELETE /racoon/:id

Deleted One (Status 200 "Found")

Nothing to Delete (Status 409 "Conflict")

Nothing to Delete (Status 410 "Gone")

{
  "result": null,
  "error" : {
    "lookup" : { "_gone": true },
    "list": [
      "message" : "The racoon you are trying to remove"
    ],
  }
}

Full Error Example

A full example with fields throwing a 400 error is below

{
  "error": {
    "lookup": {
      "username": true,
      "email": true,
    },
    "list": [
      {
        "key": "username",
        "message": "Username is required",
        "help": "https://help.example.com/create-account#username",
      },
      {
        "key": "email",
        "message": "Email is required",
        "help": "https://help.example.com/create-account#email",
      },
      {
        "key": "email",
        "message": "Email must be a valid email",
        "help": "https://help.example.com/create-account#email",
      },
    ]
  }
}

Roadmap Items

Replace redis cache middleware and files with generic caching middleware that accepts a cacheService

In @mazeltov/model implement access token scopes in tokenGrantModel and clientRole model, Incorporate this scope handling in the canAccess middleware

  • Idea for implementation: scopes relate to multiple permissions when a scope is attached to the JWT we gather distinct permissions from scopes and get the intersection of these permissions with the user permissions.

Versions

Current Tags

  • Version
    Downloads (Last 7 Days)
    • Tag
  • 1.0.8
    0
    • latest

Version History

Package Sidebar

Install

npm i @mazeltov/middleware

Weekly Downloads

0

Version

1.0.8

License

MIT

Unpacked Size

54.9 kB

Total Files

28

Last publish

Collaborators

  • jeffstraney