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

level-manifest-services

v0.1.0

Published

Extended level-manifest format for defining methods as services

Downloads

3

Readme

level-manifest-services

Extended level-manifest format for defining methods as services

This lib defines a JSON-serializable format extending the one defined by level-manifest with some additional keys for describing services built on top of leveldb. It also provides some utilities for helping to surface sublevels defined with a compatible manifest over HTTP.

JSON-Schema is used to descibe the data model of records within a subdomain, and methods can be annotated with additional data to define their behavior when exposed over HTTP.

Method Type Definitions

The types of the arguments and return for custom async methods exposed on a sublevel can be elaborated upon with schema definitions:

{
  methods: {
    myCustomMethod: {
      type: 'async',
      // json-schema tuple typing to define arguments tuple for async functions
      // TODO: variadic calls, `...rest`
      arguments: [
        { type: 'string' },
        { type: 'number' }
      ],
      // schema defining type of value in success position of invoked callback
      return: {
        // implied `type: 'object'`
        properties: {
          foo: { type: 'string' }
        }
        // TODO: variadic returns, `spread...`, symmetrical with `...rest`
      },
      // possible errors can be documented as well (as an array with implied "oneOf" semantics)
      errors: [
        {
          type: 'error'
          name: 'NotFoundError',
          code: 404,
        },
        {
          type: 'error'
          name: 'FooError',
          code: -11
        },
        {
          type: 'string' // you can also emit non-errors (but, like, don't)
        }
      ]
    }
  }
}

JSON-RPC

The above should be more than enough detail to allow clients to discover how to consume this method when exposed over a JSON-RPC endpoint. In fact, any method can be exposed via a JSON-RPC endpoint with no additional information other than a level-manifest type, but this is not terribly useful for consumers.

JSON-REST

When exposing an arbitrary manifest-defined method as an endpoint in a REST-JSON API we need a little more information -- specifically, the endpoint path must be defined explicitly. The expected HTTP method in a custom method will default to POST (this makes the fewest assumptions about the underlying request), but this can be overriden. If overriding GET or DELETE, ensure the underlying path will not conflict with keys in the database -- method paths will be tested first, so this will shadow access to your underlying keys.

The mapping of typical REST conventions to the standard levelup methods (and some optional extension methods that offer additional primitives) is fairly straight-forward:

// create or update a value by key
`PUT /{sublevels...}/{key}`

// request body with `content-type: 'application/json'`
// value as `JSON.parse` on `req.body` string
subdb.put(key, value, res)

// create a value with an auto-generated id
`POST /{sublevels...}/`

// only works if `create` exists on underlying sublevel db
subdb.create(key, value, res)

// alterantively we could defer to a key generation function
subdb.createKey(value, function (err, key) {
  if (err) return cb(err)
  subdb.put(key, value, cb)
})

// delete value by key
`DELETE /{sublevels...}/{key}`

// straightforward mapping to db's `del` method when keypath is provided
subdb.del(key, res)

// get record by key
`GET /{sublevels...}/{key}`

// value record returned as a stringified JSON object in object body
subdb.get(key, streamToString(JSON.stringifyObject(res)))

// no key path component yields a read stream
`GET /{sublevels...}/{?query}`

// query component is expected to be url-query-encoded `ltgt` query object
subdb.createReadStream(query, JSONStream.stringify(res))

// get a live stream for all changes or changes within a specific range
`SUBSCRIBE /{sublevels...}/{?query}`

// we need a base primitive for live streams in order to support this
subdb.createLiveStream(query)

// delete records within a key range
`DELETE /{sublevels...}/{?query}`

// ideally we can rely on as few base primitives as possible
util.del(subdb.createKeyStream(query))

Or we could just use levelweb, extending it to support things like SUBSCRIBE. But we still have to have a way to expose a method as a custom endpoint.

Endpoint matching

The above defines the bae fallback behavior when exposing a sublevel as a JSON-REST API. Custom methods can be annotated with an endpoint key that defines a match configuration for requests. If a request matches this configuration it will parsed and passed to the matched method:

{
  methods: {
    myCustomMethod: {
      endpoint: {
        method: 'PATCH'
      }
      // ...
    }
  }
}

Custom endpoint match tests

The functionality in the default match implementation is relatively lacking -- defining declarative patterns over HTTP requests is nontrivial. As a more powerful alternative, the match test can be provided directly, either as a function or a string which evals to the match test function implementation:

myCustomMethod: {
  endpoint: {
    // NOTE: there's no need to case-normalize `req.methods` and friends as
    // they'll be normalized before `test` is invoked
    test: function (req) { return req.method === 'PATCH' }

    // or as a string:
    test: 'function (req) { return req.method === 'PATCH' }'
  }
}

The endpoint test function will be provided a candidate req object and should return truthy if the method should be used.

Browserified modules available over HTTP may be referenced using JSON-Schema referencing:

myCustomMethod: {
  endpoint: {
    // if module is a function:
    test: { $ref: '/path/to/endpoint/test/module' }
    // or if module is an object, use JSON-Pointer to point to function
    test: { $ref: '/path/to/my/endpoint/test/module#/my/fn/key' }
  }
}

Endpoint req/res transforms

A request transform can be provided as a function or eval-able string value that should create an arguments tuple suitable for an underlying method given the provided HTTP req object. It will be invoked in the context of the source HTTP request, and will be passed the bound method as an argument. A res function may also be provided that will be invoked as the callback of an async function. It will be invoked within the context of the res stream instance.

TODO: more to work through