raveljs/ravel

View on GitHub
lib/core/decorators/resource.js

Summary

Maintainability
A
1 hr
Test Coverage
A
100%
'use strict';

const upath = require('upath');
const $err = require('../../util/application_error');
const Metadata = require('../../util/meta');

/**
 * What might be referred to as a *controller* in other frameworks, a `Resource` module defines HTTP
 * methods on an endpoint, supporting the session-per-request transaction pattern via Ravel
 * middleware. `Resource`s also support dependency injection, allowing for the easy creation of RESTful
 * interfaces to your `Module`-based application logic. Resources are really just a thin
 * wrapper around `Routes`, using specially-named handler functions (`get`, `getAll`, `head`, `headAll`,
 * `post`, `put`, `putAll`, `patch`, `patchAll`, `delete`, `deleteAll`) instead of `@mapping`. This
 * convention-over-configuration approach makes it easier to write proper REST APIs with less code,
 * and is recommended over "carefully chosen" `@mapping`s in a `Routes` class.
 *
 * Omitting any or all of the specially-named handler functions is fine, and will result in a
 * `501 NOT IMPLEMENTED` status when that particular method/endpoint is requested.
 *
 * `Resource`s inherit all the properties, methods and decorators of `Routes`. See [`Routes`](#routes)
 * for more information. Note that `@mapping` does not apply to `Resources`.
 *
 * For details concerning path construction, including how to employ parameters and regular expressions,
 * see [`Routes`](#routes).
 *
 * @param {string} basePath - The base path for all endpoints in this class. Should be unique within an application.
 * @example
 * const inject = require('ravel').inject;
 * const Resource = require('ravel').Resource;
 * const before = Resource.before;
 *
 * // you can inject your own Modules and npm dependencies into Resources
 * // @Resource('/person')
 * // @inject(koa-bodyparser', 'fs', 'custom-module')
 * class PersonResource {
 *   constructor (bodyParser, fs, custom) {
 *     this.bodyParser = bodyParser(); // make bodyParser middleware available
 *     this.fs = fs;
 *     this.custom = custom;
 *   }
 *
 *   // will map to GET /person
 *   // @before('bodyParser') // use bodyParser middleware before handler
 *   async getAll (ctx) {
 *     // ctx is a koa context object.
 *     // await on Promises, and set ctx.body to create a body for response
 *     // "OK" status code will be chosen automatically unless configured via ctx.status
 *     // Extend and throw a Ravel.Error to send an error status code
 *   }
 *
 *   // will map to GET /person/:id
 *   async get (ctx) {
 *     // can use ctx.params.id in here automatically
 *   }
 *
 *   // will map to HEAD /person
 *   async headAll (ctx) {}
 *
 *   // will map to HEAD /person/:id
 *   async head (ctx) {}
 *
 *   // will map to POST /person
 *   async post (ctx) {}
 *
 *   // will map to PUT /person
 *   async putAll (ctx) {}
 *
 *   // will map to PUT /person/:id
 *   async put (ctx) {}
 *
 *   // will map to PATCH /person
 *   async patchAll (ctx) {}
 *
 *   // will map to PATCH /person/:id
 *   async patch (ctx) {}
 *
 *   // will map to DELETE /person
 *   async deleteAll (ctx) {}
 *
 *   // will map to DELETE /person/:id
 *   async delete (ctx) {}
 * }
 *
 * module.exports = PersonResource;
 */
const Resource = function (basePath) {
  if (typeof basePath !== 'string') {
    throw new $err.IllegalValue(
      '@Resource must be used with a basePath, as in @Resource(\'/route/base/path\')');
  }
  // normalize and validate base path
  const bp = upath.toUnix(upath.posix.normalize(basePath));
  return function (target, key, descriptor) {
    Metadata.putClassMeta(target.prototype, '@role', 'type', 'Resource');
    Metadata.putClassMeta(target.prototype, '@role', 'name', bp);
  };
};

/*!
 * Populates a class with a static reference to the // @Resource role decorator
 */
module.exports = Resource;