0.3.11 • Published 4 years ago

cascading v0.3.11

Weekly downloads
-
License
Apache-2.0
Repository
github
Last release
4 years ago

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.

0.3.11

4 years ago

0.3.10

4 years ago

0.3.9

4 years ago

0.3.8

4 years ago

0.3.7

4 years ago

0.3.6

4 years ago

0.3.5

4 years ago

0.3.4

4 years ago

0.3.3

4 years ago

0.3.2

4 years ago

0.3.1

4 years ago

0.3.0

4 years ago

0.2.2

8 years ago

0.2.0

8 years ago

0.1.1

8 years ago

0.1.0

8 years ago