npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@mazeltov/middleware

v1.0.8

Published

Middleware for Mazeltov project

Downloads

3

Readme

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.