@priestine/grace
v2.0.1
Published
Minimalistic middleware-based framework for building web apps with Node.js
Downloads
1
Maintainers
Readme
@priestine/grace
Minimalistic middleware-based framework for building web apps with Node.js.
Installation
npm i --save @priestine/data @priestine/routing @priestine/grace
or
yarn add @priestine/data @priestine/routing @priestine/grace
Overview
@priestine/grace
is a set of helper code to speed up development with @priestine/routing
.
It is going to include most common things wrapped up into middleware, pipelines or even routers
that you can concat to your code where necessary.
Usage
Errors
HttpError
is a class that extends Error
and you can use it to exit the pipeline with required
status code and error message. It provides chaining methods for setting up the response you want to send:
const { HttpError } = require('@priestine/grace');
throw new HttpError().withStatusCode(400).withMessage('Missing required field "id"');
// or
reject(new HttpError().withStatusCode(400).withMessage('Missing required field "id"'));
NOTE: HttpError
is not intended to be used for debugging or serving as a wrapper for ambiguous errors you do not anticipate.
There is a set of predefined errors with appropriate status codes assigned so you can use them and extend with required error messages. Refer to RFC or MDN for the description of status codes.
const { BadRequestError } = require('@priestine/grace');
throw BadRequestError.withMessage('No-no-no');
4xx Errors
BadRequestError
(400)UnauthorizedError
(401)ForbiddenError
(402)NotFoundError
(404)MethodNotAllowedError
(405)NotAcceptableError
(406)ProxyAuthenticationRequiredError
(407)RequestTimeoutError
(408)ConflictError
(409)GoneError
(410)LengthRequiredError
(411)PreconditionFailedError
(412)PayloadTooLargeError
(413)URITooLongError
(414)UnsupportedMediaTypeError
(415)RequestedRangeNotSatisfiableError
(416)ExpectationFailedError
(417)MisdirectedRequestError
(421)UnprocessableEntityError
(422)LockedError
(423)FailedDependencyError
(424)TooEarlyError
(425)UpgradeRequiredError
(426)PreconditionRequiredError
(428)TooManyRequestsError
(429)RequestHeaderFieldsTooLargeError
(431)UnavailableForLegalReasonsError
(451)
5xx Errors
InternalServerError
(500)NotImplementedError
(501)BadGatewayError
(502)ServiceUnavailableError
(503)GatewayTimeoutError
(504)HttpVersionNotSupportedError
(505)VariantAlsoNegotiatesError
(506)InsufficientStorageError
(507)LoopDetectedError
(508)NotExtendedError
(510)NetworkAuthenticationRequiredError
(511)
Pipelines
AccessControlPipeline
Access control pipeline assigns the following response headers:
- Access-Control-Allow-Origin
- Access-Control-Allow-Methods
- Access-Control-Allow-Headers (optional)
- Access-Control-Expose-Headers (optional)
Accepted arguments
/**
* @interface AccessControlPipelineOpts
*/
export interface AccessControlPipelineOpts {
/**
* Origin to be set to Access-Control-Allow-Origin value.
*/
origin: string;
/**
* Top-level HttpRouter for detecting methods available for current route.
*/
router: HttpRouter;
/**
* Headers allowed to be sent in request.
*/
headers?: string[];
/**
* Headers allowed to be referenced from browser.
*/
exposeHeaders?: string[];
}
Usage
const { Pipeline } = require('@priestine/data/src');
const { AccessControlPipeline } = require('@priestine/grace');
const router = require('../routing').MainRouter;
const MyPipeline = Pipeline.empty()
.concat(
AccessControlPipeline({
origin: '*',
router,
headers: ['Accept', 'Content-Type', 'Authorization', 'X-Request-ID', 'If-None-Match'],
exposeHeaders: ['ETag'],
})
)
.concat(/**/);
AuthorizationHeaderPipeline
Authorization header pipeline checks if Authorization header is in place in IncomingMessage and assigns its value
to ctx.intermediate.authorizationHeaderValue
.
If the header is missing, it throws UnauthorizedError
.
If the header is invalid (auth type wrong or no value), it throws ForbiddenError
.
NOTE: This pipeline is agnostic and you need to unpack the token or whatever the auth string is yourself.
For TypeScript developers, it provides helper AuthorizationHeaderAware
interface to be passed to generic HttpContextInterface
.
import { HttpContextInterface } from '@priestine/routing';
import { AuthorizationHeaderAware } from '@priestine/grace';
const MyAuthRelatedMiddleware = (ctx: HttpContextInterface<AuthorizationHeaderAware>) => {};
Accepted arguments
/**
* @interface AuthorizationHeaderPipelineOpts
*/
export interface AuthorizationHeaderPipelineOpts {
/**
* Authentication type.
*/
authType: WWWAuthenticateType;
/**
* Custom messages to be displayed in case error occurs.
*/
errors?: {
/**
* Error if request does not have Authorization header.
* @default UnauthorizedError
*/
unauthorized?: HttpError;
/**
* Error if request has invalid Authorization header.
* @default ForbiddenError
*/
forbidden?: HttpError;
};
}
Usage
const { Pipeline } = require('@priestine/data/src');
const { AuthorizationHeaderPipeline, UnauthorizedError, ForbiddenError } = require('@priestine/grace');
const MyPipeline = Pipeline.empty()
.concat(
AuthorizationHeaderPipeline({
authType: 'Bearer',
errors: {
unauthorized: UnauthorizedError.withMessage('Log in to get access to this resource'),
forbidden: ForbiddenError.withMessage('You shall not pass'),
},
})
)
.concat(/**/);
EndResponseBodyPipeline
DEPRECATED: Will be renamed to
SendResponseBodyPipeline
End response by sending the contents of ctx.intermediate.responseBody
to the client.
If response is finished and an error occurred, the error will be put to stdout. If response is finished and no error happened, the pipeline does nothing.
For TypeScript developers, it provides helper ResponseBodyAware
interface to be passed to generic HttpContextInterface
.
import { HttpContextInterface } from '@priestine/routing';
import { ResponseBodyAware } from '@priestine/grace';
const MyResponseRelatedMiddleware = (ctx: HttpContextInterface<ResponseBodyAware>) => {};
ResponseBodyAware
is generic and provided type is referenced by intermediate.responseBody
, e.g.:
import { HttpContextInterface } from '@priestine/routing';
import { ResponseBodyAware } from '@priestine/grace';
const MyResponseRelatedMiddleware = (ctx: HttpContextInterface<ResponseBodyAware<{ id: number }>>) => {};
Accepted arguments
/**
* @interface EndResponsePipelineOpts
*/
export interface EndResponsePipelineOpts {
/**
* Flag for the pipeline to JSON.stringify responseBody.
*/
json?: boolean;
/**
* Flag for wrapping response body into `{ success: boolean, data: <responseBody> }`.
*/
wrap?: boolean;
}
Usage
const { Pipeline } = require('@priestine/data/src');
const { EndResponseBodyPipeline } = require('@priestine/grace');
const MyPipeline = Pipeline.empty()
.concat(/**/)
.concat(
EndResponseBodyPipeline({
json: true,
})
);
EndEmptyResponsePipeline
DEPRECATED: Will be renamed to
SendEmptyResponsePipeline
End response with empty string ("").
If response is finished and an error occurred, the error will be put to stdout. If response is finished and no error happened, the pipeline does nothing.
Usage
const Pipeline = require('@priestine/data/src').Pipeline;
const MyPipeline = Pipeline.empty()
.concat(/**/)
.concat(EndEmptyResponsePipeline);
Utils
getFromEnv
Simple function that returns contents of process.env
for given key, or the default value.
const { getFromEnv } = require('@priestine/grace');
getFromEnv('MY_ENV_VAR', 'default_value');
CaseTransformer
DEPRECATED: Will be moved to external package
CaseTransformer is a tool for transforming string from one case to another. Supported cases are:
- camelCase
- PascalCase
- kebab-case
- snake_case
- dot.case
- colon:case
CaseTransformer can be used itself using its of
pointer interface:
const { CaseTransformer } = require('@priestine/grace');
const helloWorld = CaseTransformer.of('hello-world').from.kebab.to.camel;
console.log(helloWorld); // helloWorld
Alternatively, you can use one of many helper functions:
transformCase(str: string)
transformCase
is a pointer interface for lifting a string into transformation which is built via
fluent interface chaining.
const { transformCase } = require('@priestine/grace');
console.log(transformCase('helloWorld').from.camel.to.snake); // hello_world
toXCase(strs: string[])
Transforms array of strings to a string with given case. Supported helpers are:
- toCamelCase
- toKebabCase
- toPascalCase
- toSnakeCase
- toDotCase
- toColonCase
// Example
const { toDotCase } = require('@priestine/grace');
toDotCase(['http', 'errors', 'access_denied']); // 'http.errors.access_denied'
fromXCase(str: string)
Transforms string in specified case to an array of separate strings. Supported helpers are:
- fromCamelCase
- fromKebabCase
- fromPascalCase
- fromSnakeCase
- fromDotCase
- fromColonCase
// Example
const { fromDotCase } = require('@priestine/grace');
const myTranslationsObject = require('./en_US.json');
R.path(fromDotCase('http.errors.access_denied'), myTranslationsObject);
Middleware
Response-specific
SetHeader
A set of middleware for setting response headers. Each function accepts a value and returns a function that
accepts the HttpContextInterface
.
const { Pipeline } = require('@priestine/data/src');
const { SetContentTypeHeader, SetContentLanguageHeader, SetDateHeader } = require('@priestine/grace');
const MyHeadersPipeline = Pipeline.from([
SetContentTypeHeader('application/json'),
SetContentLanguageHeader('en_US'),
SetDateHeader(new Date().toUTCString()),
]);
List of supported SetHeader middleware
- SetAcceptPatchHeader
- SetAcceptRangesHeader
- SetAllowHeader
- SetAgeHeader
- SetCacheControlHeader
- SetConnectionHeader
- SetContentTypeHeader
- SetContentDispositionHeader
- SetContentEncodingHeader
- SetContentLanguageHeader
- SetContentLengthHeader
- SetContentLocationHeader
- SetContentRangeHeader
- SetDateHeader
- SetETagHeader
- SetLastModifiedHeader
- SetLinkHeader
- SetLocationHeader
- SetProxyAuthenticateHeader
- SetRetryAfterHeader
- SetServerHeader
- SetSetCookieHeader
- SetStrictTransportPolicyHeader
- SetTransferEncodingHeader
- SetUpgradeHeader
- SetVaryHeader
- SetViaHeader
- SetWarningHeader
- SetWWWAuthenticateHeader
- SetXRequestIDHeader
- SetAccessControlAllowOriginHeader
- SetAccessControlAllowMethodsHeader
- SetAccessControlAllowHeadersHeader
- SetAccessControlExposeHeadersHeader
You can use these pre-defined middleware for building custom logic, e.g.:
const uuid = require('uuid/v4');
const { SetXRequestIDHeader, CheckAcceptHeader, SetContentTypeHeader } = require('@priestine/grace');
const { Pipeline } = require('@priestine/data/src');
/**
* Simple example.
* Sign each request with new UUID value.
*/
const SignRequest = SetXRequestIDHeader(uuid());
/**
* A bit more complicated example.
* Check if client accepts application/json and fallback to application/xml if it doesn't.
* We don't need any checks as we have applied them in the previous step of the pipeline (see below).
*/
const AssignContentType = (ctx) => {
const acceptable = ctx.request.headers.accept.split(', ');
const json = acceptable.includes('*/*') || acceptable.includes('application/json');
return SetContentTypeHeader(json ? 'application/json' : 'application/xml')(ctx);
};
const MyPipeline = Pipeline.from([
// Check Accept header exists and has supported value
CheckAcceptHeader(['*/*', 'application/json', 'application/xml']),
// Sign request with UUID
SignRequest,
// Assign response Content-Type header
AssignContentType,
]);
TransformResponseKeys
Transform keys of response object with given transformer function. The transformation is applied recursively.
const { Pipeline } = require('@priestine/data/src');
const { TransformResponseObjectKeys, EndResponseBodyPipeline } = require('@priestine/grace');
const MyPipeline = Pipeline.of(TransformResponseObjectKeys((x) => x.toUpperCase())).concat(
EndResponseBodyPipeline({ json: true })
);
The middleware has two helpers for common cases:
- TransformResponseObjectKeysFromCamelToSnake
- TransformResponseObjectKeysFromSnakeToCamel
Request-specific
CheckAcceptHeader
Check if request Accept
header value is defined and supported. Throws NotAcceptableError
if header value is not
acceptable by the server.
This middleware accepts the following arguments:
- acceptable: array of strings representing MIME-types acceptable by the server, e.g.
["application/json", "application/xml"]
, defaults to['*/*']
- error: optional error to be thrown in case MIME-type is not acceptable (defaults to
NotAcceptableError.withMessage('not acceptable'))
If acceptable includes '*/*'
, any value of Accept
header does not trigger the error. You can limit that to
['application/json']
if you are building JSON API.
const { Pipeline } = require('@priestine/data/src');
const { CheckAcceptHeader } = require('@priestine/grace');
const MyPipeline = Pipeline.of(CheckAcceptHeader(['application/json']));
ValidateObjectBodyProp
Validate request body object with given set of validators by given request body key. This middleware should be used when object is expected in request body.
@priestine/grace
provides a set of common data-type validators. You can use any other validator, e.g.
provided by Ramda or Validator.js - validators are functions that accept a value and return a boolean.
Validators provided by the package:
isInstanceOf
isObject
isString
isBoolean
isTrue
isFalse
isNumber
isInteger
isFloat
isEmpty
isRequired - fails if value is falsy
isNull
isUndefined
isFunction
isIn - fails if value is not in the array provided in the argument
lt - fails if value is greater than or equal to the one provided in the argument
lte - fails if value is greater than the one provided in the argument
gt - fails if value is less than or equal to the one provided in the argument
gte - fails if value is less than the one provided in the argument
isOptional - applies validators provided in the argument only if the value is defined
negate - negates the result of validation (e.g.
negate(isNull)
fails if value is null) This function is added for composing notX validators.
ValidateObjectBodyProp
provides generic interface for type-hinting.
Accepted arguments
/**
* @interface ValidateBodyOpts
*/
export interface ValidateBodyOpts<T, K extends keyof T = keyof T> {
/**
* requestBody key to be referenced for validating its value.
*/
key: K;
/**
* Array of validators to be applied to the value.
*/
validators: Array<(value: T[K]) => boolean>;
/**
* Custom error to be thrown.
* @default BadRequestError
*/
error?: HttpError;
}
Example
import { Pipeline } from '@priestine/data/src';
import { ValidateObjectBodyProp, isRequired, isInteger, isOptional, isString } from '@priestine/grace';
interface ExpectedBody {
id: number;
firstName: string;
}
const MyPipeline = Pipeline.from([
ValidateObjectBodyProp<ExpectedBody>('id', [isRequired, isInteger]),
ValidateObjectBodyProp<ExpectedBody>('firstName', [isOptional([isString])]),
]);
ValidateArrayBodyProp
ValidateArrayBodyProp is the same as ValidateObjectBodyProp with the only difference - it should be used when array request body is expected as it applies validation recursively on each element in the array.
Accepted arguments
/**
* @interface ValidateBodyOpts
*/
export interface ValidateBodyOpts<T, K extends keyof T = keyof T> {
/**
* requestBody key to be referenced for validating its value.
*/
key: K;
/**
* Array of validators to be applied to the value.
*/
validators: Array<(value: T[K]) => boolean>;
/**
* Custom error to be thrown.
* @default BadRequestError
*/
error?: HttpError;
}
Example
import { Pipeline } from '@priestine/data/src';
import { ValidateArrayBodyProp, isRequired, isInteger, isOptional, isString } from '@priestine/grace';
interface ExpectedBodyArrayItem {
id: number;
firstName: string;
}
const MyPipeline = Pipeline.from([
ValidateArrayBodyProp<ExpectedBodyArrayItem>({ key: 'id', validators: [isRequired, isInteger] }),
ValidateArrayBodyProp<ExpectedBodyArrayItem>({ key: 'firstName', validators: [isOptional([isString])] }),
]);
ValidateBodyProp
Apply ValidateArrayBodyProp
or ValidateObjectBodyProp
depending on whether request body is an array or an object.
If a primitive is passed, throws BadRequestError
.
If validation is not passed, throws BadRequestError
.
Accepted arguments
/**
* @interface ValidateBodyOpts
*/
export interface ValidateBodyOpts<T, K extends keyof T = keyof T> {
/**
* requestBody key to be referenced for validating its value.
*/
key: K;
/**
* Array of validators to be applied to the value.
*/
validators: Array<(value: T[K]) => boolean>;
/**
* Custom error to be thrown.
* @default BadRequestError
*/
error?: HttpError;
}
Example
import { Pipeline } from '@priestine/data/src';
import { ValidateBodyProp, isRequired, isInteger, isOptional, isString } from '@priestine/grace';
interface ExpectedObjectOrArrayItem {
id: number;
firstName: string;
}
const MyPipeline = Pipeline.from([
ValidateBodyProp<ExpectedObjectOrArrayItem>({ key: 'id', validators: [isRequired, isInteger] }),
ValidateBodyProp<ExpectedObjectOrArrayItem>({ key: 'firstName', validators: [isOptional([isString])] }),
]);
ExtractJSONRequestBody
Extract body from the IncomingMessage
. This middleware unpacks the data by piping request to
JSONStream
, then to event-stream.map
and finally puts the request body to intermediate.requestBody
.
For TypeScript developers, it provides helper RequestBodyAware<T = {}>
interface to be passed to generic HttpContextInterface
.
import { HttpContextInterface } from '@priestine/routing';
import { RequestBodyAware } from '@priestine/grace';
const MyRequestBodyAwareMiddleware = (ctx: HttpContextInterface<RequestBodyAware>) => {};
Accepted arguments
- requestTimeout: amount of time in milliseconds until request timeout.
Example
const { Pipeline } = require('@priestine/data/src');
const { ExtractJSONRequestBody } = require('@priestine/grace');
const MyPipeline = Pipeline.of(ExtractJSONRequestBody(90000));
ExtractRequestParams
Extract request params from the IncomingMessage
if route was registered with RegExp and puts them
to intermediate.requestParams
as array.
Example
const { Pipeline } = require('@priestine/data/src');
const { ExtractRequestParams } = require('@priestine/grace');
const MyPipeline = Pipeline.of(ExtractRequestParams);
TransformRequestKeys
Transform keys of request object with given transformer function. The transformation is applied recursively.
const { Pipeline } = require('@priestine/data/src');
const { TransformRequestObjectKeys, EndRequestBodyPipeline } = require('@priestine/grace');
const MyPipeline = Pipeline.of(TransformRequestObjectKeys((x) => x.toUpperCase())).concat(
EndRequestBodyPipeline({ json: true })
);
The middleware has two helpers for common cases:
- TransformRequestObjectKeysFromCamelToSnake
- TransformRequestObjectKeysFromSnakeToCamel