cascading v0.3.11
node-cascading
Yet another web framework written in TypeScript with some toolkit, e.g. singleton factory and unit test framework. It is compiled to ES5.
Install
npm install cascading
Example handler
// File 'sub_handler.ts'.
import { CONTENT_TYPE_TEXT, HttpMethod } from 'cascading/common';
import { HttpHandler } from 'cascading/http_handler';
import { SingletonFactory } from 'cascading/singleton_factory';
class SubHandler implements HttpHandler {
public method = HttpMethod.GET;
public urlRegex: RegExp;
public init(): void {
this.urlRegex = /^\/.*$/;
}
public handle(logContext: string, request: http.IncomingMessage, parsedUrl: url.Url): Promise<HttpResponse> {
return Promise.resolve({
contentType: CONTENT_TYPE_TEXT,
content: '<html>...</html>',
});
}
}
export let SUB_HANDLER_FACTORY = new SingletonFactory((): SubHandler => {
let handler = new SubHandler();
handler.init();
return handler;
});
Then added to router.
import { ROUTER_FACTORY } from 'cascading/router';
import { SUB_HANDLER_FACTORY } from './sub_handler';
let router = ROUTER_FACTORY.get('your-hostname.com');
router.addHandler(SUB_HANDLER_FACTORY.get());
logContext
simply contains a random request id for easy log tracking when you prepend it to any subsequent logging. No need to add a space.
Start router
import { ROUTER_FACTORY } from 'cascading/router';
let router = ROUTER_FACTORY.get('your-hostname.com');
// Starts a HTTP server.
router.start();
import { ROUTER_FACTORY } from 'cascading/router';
let router = ROUTER_FACTORY.get('your-hostname.com', {
key: privateKey,
cert: certificate,
ca: [ca...],
});
// Starts a HTTP & HTTPS server. All HTTP requests will be redirected to HTTPS temporarily (Code
// 307). Refer to Node's document for https.ServerOptions.
router.start();
Ports are fixed at 80 for HTTP and 443 for HTTPS.
Logger
import { LOGGER } from 'cascading/logger';
LOGGER.info(...);
LOGGER.warning(...);
LOGGER.error(...);
LOGGER
, also used by Router
, depends on GCP (Google Cloud Platform) logging lib. It will try to log to GCP all the time as well as log to console, ignoring any error regarding GCP. Internally, it holds a buffer of 100 messages before flushing to GCP, or waits for 30 secs upon receiving the first message.
Cross-origin request & Preflight handler
This router always allows cross-origin requests from any origin. Depends on client implementation, you might receive an OPTIONS
request to ask for cross-origin policy. Each router always has a singleton PreflightHandler
added when created from ROUTER_FACTORY
. However, it intercepts all OPTIONS
requests. Thus any handler that handles OPTIONS
is ignored.
Static file & directory handler
import { STATIC_DIR_HANDLER_FACTORY, STATIC_FILE_HANDLER_FACTORY } from 'cascading/static_handler';
// ...
router.addHandler(STATIC_FILE_HANDLER_FACTORY.get('/favicon.ico', 'image/favicon.ico'));
router.addHandler(STATIC_DIR_HANDLER_FACTORY.get('/image', 'image'));
// ...
STATIC_FILE_HANDLER_FACTORY
takes a URL and a local path. STATIC_DIR_HANDLER_FACTORY
takes a URL prefix and a local directory.
SingletonFactory
import { SingletonFactory } from 'cascading/singleton_factory';
// ...
export let SUB_HANDLER_FACTORY = new SingletonFactory((): SubHandler => {
let handler = new SubHandler();
handler.init();
return handler;
});
SingletonFactory
takes a function without any argument to construct an instance. It will only call the the constucting function once, no matter what.
Test base
import { TestCase, runTests, assert, assertContains, assertError, expectRejection, expectThrow } from 'cascading/test_base';
// ...
class FileHandlerSuffix implements TestCase {
public name = 'FileHandlerSuffix';
public async execute() {
// Prepare
let handler = new StaticFileHandler('/url', 'path.js');
handler.init();
// Execute
let response = await handler.handle(undefined, undefined, undefined);
// Verify
assert(response.contentFile === 'path.js');
assert(response.contentType === 'text/javascript');
assertContains(response.contentType, 'javas');
}
}
// ...
runTests('StaticHttpHandlerTest', [
new FileHandlerSuffix(),
// ...
]);
Compile and execute this file normally using tsc
and node
to run all tests added in runTests()
.
To run a single test, use node test_file.js <No. of test case>
.
In addition, use expectRejection
, expectThrow
and assertError
to test failure cases. Note that assertError
first asserts the error is an instance of JavaScript Error
.
{
// Expect rejection from a promise.
let promise: Promise<any> = ...
let error = await expectRejection(promise);
// The message of `error` only needs to contain the message of `expectedError`.
assertError(error, expectedError);
}
{
// Expect an error to be thrown when invoking foo().
let error = expectThrow(() => foo());
// The message of `error` only needs to contain the message of `expectedError`.
assertError(error, expectedError);
}
TypedError
import { ErrorType, TypedError, newInternalError, newUnauthorizedError } from 'cascading/errors';
let error1 = newInternalError('Error');
error1.errorType === ErrorType.Internal;
let nativeError = new Error('Failure');
let error2 = newUnauthorizedError('Error', nativeError);
error2.errorType === ErrorType.Unauthorized;
TypedError
requires an ErrorType
and optionally wraps an existing error by prepending the new error message to it. The ErrorType
of a TypedError
is erased when passed to another TypedError
. The value of ErrorType
reflects HTTP error code. When a HttpHandler
returns a TypedError
, the number value of its ErrorType
is passed to response.
Data interface
In some cases, we want to safely cast an any
object into a typed object, e.g., validating a data object received from wire. Because TypeScript/JavaScript doesn't have type information in runtime, one cannot pass an interface/class itself to a function to validate each field (i.e. no reflection). And we also don't want to write a validation function for each interface/class. One could consider using Protocol Buffers but I want a really light-weight version.
Write the interface definition in a .itf file, which is essentially a JSON object. Examples are shown below.
[{
"object": {
"name": "TestObject",
"fields": [{
"name": "field1",
"type": "string"
}, {
"name": "field2",
"type": "number"
}, {
"name": "field3",
"type": "boolean"
}]
}
}, {
"enum": {
"name": "TestEnumColor",
"values": [{
"name": "Red",
"value": 1
}, {
"name": "Green",
"value": 3
}, {
"name": "Blue",
"value": 10
}]
}
}]
[{
"object": {
"name": "TestNestedObject",
"fields": [{
"name": "nestedField1",
"type": "TestObject",
"importFrom": "./test_interface"
}, {
"name": "color",
"type": "TestEnumColor",
"importFrom": "./test_interface"
}, {
"name": "color2",
"type": "TestEnumColor",
"importFrom": "./test_interface"
}]
}
}]
.itf needs to start with an array, whose element is either an "object" or an "enum" definition. cli/interface_generator/object_generator.ts & enum_generator.ts contain the definitions of an "object" and an "enum" interface respectively. "importFrom" will be copied to the generated TypeScript file without modification.
The generated TypeScript file will be located in the same directory with the same file name but with .ts as its file extension. It exports TypeScript interface
or enum
definitions as well as a function construct<name of the object>(obj?: any)
for each definition.
Execute cascading build
(without arguments) in your command line to build all .itf files.
CLI
Build
By executing cascading build
, which doesn't accept any arguments, it first builds all .itf files and then compile all TypeScript files.
Run
By executing cascading run <file path without extension> <all pass along arguments>
, it first builds all files and then runs the built JavaScript file. Starting from the 4th arguments, they will be passed as-is to the executed JavaScript file.
Format
By executing cascading fmt <file path without extension>
, it sorts (sadly only sorting) the import statements in a deterministic way.