@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"');
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
export interface AccessControlPipelineOpts {
origin: string;
router: HttpRouter;
headers?: string[];
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();
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
export interface AuthorizationHeaderPipelineOpts {
authType: WWWAuthenticateType;
errors?: {
unauthorized?: HttpError;
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
export interface EndResponsePipelineOpts {
json?: boolean;
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);
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);
toXCase(strs: string[])
Transforms array of strings to a string with given case. Supported helpers are:
- toCamelCase
- toKebabCase
- toPascalCase
- toSnakeCase
- toDotCase
- toColonCase
const { toDotCase } = require('@priestine/grace');
toDotCase(['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
const { fromDotCase } = require('@priestine/grace');
const myTranslationsObject = require('./en_US.json');
R.path(fromDotCase('http.errors.access_denied'), myTranslationsObject);
Middleware
Response-specific
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()),
]);
- 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');
const SignRequest = SetXRequestIDHeader(uuid());
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([
CheckAcceptHeader(['*/*', 'application/json', 'application/xml']),
SignRequest,
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
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
export interface ValidateBodyOpts<T, K extends keyof T = keyof T> {
key: K;
validators: Array<(value: T[K]) => boolean>;
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
export interface ValidateBodyOpts<T, K extends keyof T = keyof T> {
key: K;
validators: Array<(value: T[K]) => boolean>;
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
export interface ValidateBodyOpts<T, K extends keyof T = keyof T> {
key: K;
validators: Array<(value: T[K]) => boolean>;
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));
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