kite-framework
Version:
Modern, fast, flexible HTTP-JSON-RPC framework
366 lines (365 loc) • 14.5 kB
JavaScript
"use strict";
/***
* Copyright (c) 2017 [Arthur Xie]
* <https://github.com/kite-js/kite>
*
* Permission is hereby granted, free of charge, to any person obtaining a copy
* of this software and associated documentation files (the "Software"), to deal
* in the Software without restriction, including without limitation the rights
* to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
* copies of the Software, and to permit persons to whom the Software is
* furnished to do so, subject to the following conditions:
*
* The above copyright notice and this permission notice shall be included in
* all copies or substantial portions of the Software.
*/
Object.defineProperty(exports, "__esModule", { value: true });
exports.Kite = void 0;
const version_1 = require("./core/version");
const error_1 = require("./core/error");
const log_service_1 = require("./core/log.service");
const error_service_1 = require("./core/error.service");
const controller_factory_1 = require("./core/controller.factory");
const callsite_1 = require("./core/callsite");
const parsesize_1 = require("./utils/parsesize");
const http_router_1 = require("./utils/http.router");
const watch_service_1 = require("./core/watch.service");
const URL = require("url");
const path = require("path");
const default_config_1 = require("./default.config");
const error_codes_1 = require("./core/error.codes");
const http_1 = require("http");
/**
* Kite
*/
class Kite {
constructor(config = {}) {
this.errorService = new error_service_1.ErrorService();
this.middlewares = new Set();
this.workdir = callsite_1.getCallerPath();
this.log('Kite framework ver ' + version_1.VERSION);
this.log(`Working at directory ${this.workdir}`);
if (typeof config === 'string') {
config = path.isAbsolute(config) ? config : path.join(this.workdir, config);
config = require.resolve(config);
this.log(`Loading configuration from file "${config}"`);
}
else {
this.log('Loading configuration from object');
}
this._init(config);
this.log('Creating server');
// Create server
let server = this.server = http_1.createServer((request, response) => {
response.on('error', (err) => {
this.logService.error(err);
});
this.onRequest(request, response);
});
server.on('clientError', (err, socket) => {
console.error(err);
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
server.on('error', (err) => {
this.log(`*** ERROR *** ${err.message}`);
if (err.code === 'EADDRINUSE') {
this.log('*** ERROR *** address:port in use, please change "hostname / port" for Kite, or close the conflicting process');
}
this.log('EXIT');
process.exit();
});
this.log('Ready to fly');
}
/**
* Load the configuration from file or an KiteConfig object
*
* @param { string | Config } config if string is given, it's treated as a filename, configuration will from this file,
* if a KiteConfig object is given, configuration will load from this object
*
* @private
*/
_init(config) {
let cfg = config;
if (typeof config === 'string') {
try {
cfg = require(config).kiteConfig;
}
catch (e) {
throw new Error(`Failed to load config file "${config}": ${e.message}`);
}
}
// Combine default configuration with user configuration
cfg = Object.assign({}, default_config_1.DefaultConfig, cfg);
this.maxContentLength = parsesize_1.parseSize(cfg.maxContentLength);
// concact log filenames with working root directory if they are not a absolute path
if (typeof cfg.log.out === 'string' && !path.isAbsolute(cfg.log.out)) {
cfg.log.out = path.join(this.workdir, cfg.log.out);
}
if (typeof cfg.log.err === 'string' && !path.isAbsolute(cfg.log.err)) {
cfg.log.err = path.join(this.workdir, cfg.log.err);
}
// start log service
this.logService = new log_service_1.LogService(cfg.log.level, cfg.log.out, cfg.log.err);
// set default router
if (cfg.router) {
this.log('** warning ** config.router will be deprecated, use kite.route() instead');
this.route(cfg.router);
}
else {
this.route();
}
// built-in errors
this.errorService.errors = Object.assign({}, error_codes_1.ERROR_CODES);
// combine with user errors
if (cfg.errors) {
Object.assign(this.errorService.errors, cfg.errors);
}
// build data hub from factory
this.receivers = {};
if (cfg.receiverProvider) {
let receivers = [];
if (typeof cfg.receiverProvider === 'function') {
receivers.push(cfg.receiverProvider);
}
else if (Array.isArray(cfg.receiverProvider)) {
receivers = cfg.receiverProvider;
}
receivers.forEach(provider => {
let { contentType, receiver } = provider.call(null);
this.receivers[contentType] = receiver;
});
}
if (!this.controllerFactory) {
this.controllerFactory = new controller_factory_1.ControllerFactory();
this.controllerFactory.workdir = this.workdir;
}
// this.controllerFactory.logService = this.logService;
if (!this.watchService) {
this.watchService = new watch_service_1.WatchService(__dirname);
this.watchService.logService = this.logService;
this.watchService.setWorkDir(this.workdir);
this.controllerFactory.watchService = this.watchService;
}
this.config = cfg;
Object.seal(this.config);
Object.freeze(this.config);
if (typeof config === 'string') {
this.watchConfigFile(config);
}
}
/**
* Set watch mode
* Watch mode allows you code & test APIs smoothly without restarting Kite server.
*
* @param flag - true (default), Kite work in watch mode, suggested in dev mode
* - false, turn off watch suggested set to `false` in production
*/
watch(flag = true) {
this.watchService.setEnabled(flag);
return this;
}
/**
* Watch configuration file
* @param filename
*/
watchConfigFile(filename) {
// watch for config file changing
this.watchService.watch(filename, (configFilename) => {
this.log('Reload configuration');
this._init(configFilename);
});
}
/**
* Relase your kite, let it fly
* @param port server listen port
* @param host server listen host
* @param callback optional, if callback is provided, it wil be called after kite application server started
*/
fly(port = 4000, host = 'localhost', callback) {
if (this.server && this.server.listening) {
this.server.close();
}
this.server.listen(port, host, () => {
let { address, port } = this.server.address();
this.log(`Flying! server listening at ${address}:${port}`, '\x1b[33m');
if (callback) {
callback();
}
});
return this;
}
/**
* Request listener, process all requests here
* @param { IncomingMessage } request
* @param { ServerResponse } response
*/
async onRequest(request, response) {
try {
let url = URL.parse(request.url, true), inputs = url.query, // URL query string
filename = this.router.map(url, request.method), // map to actual filename
scope;
// api = await this.controllerFactory.get(filename); // get controller instance
// metadata: ControllerMetadata = getControllerMetadata(api.constructor),
let controller = this.controllerFactory.getController(filename);
if (this._provider) {
scope = this._provider.exec(request, controller, inputs);
}
let api = await this.controllerFactory.getInstance(controller, scope);
// if api handle http request it self, skip "input" parsing
if ('onRequest' in api) {
inputs = await api.onRequest(request, url.query);
}
else if ((request.headers['content-length'] || request.headers['transfer-encoding']) &&
request.method !== 'GET' &&
request.method !== 'TRACE') {
// if there is any message-body sent from client, try to parse it
// an entity-body is explicitly forbidden in TRACE, and ingored in GET
let [contentType, charset] = (request.headers['content-type'] || 'text/plain').split(';'), entityBody = await this.getEntityBody(request);
if (charset) {
charset = charset.split('=')[1];
}
if (this.receivers[contentType]) {
try {
let data = this.receivers[contentType](entityBody, charset);
inputs = Object.assign({}, url.query, data);
}
catch (e) {
this.logService.error(e);
throw new error_1.KiteError(1010);
}
}
else {
this.logService.warn(`Unsupported content type "${contentType}"`);
inputs.$data = entityBody;
}
}
// Call middlewares
let middleResult;
for (let middleware of this.middlewares) {
middleResult = middleware.exec(request, response, api, inputs);
// if return type is promise then wait promise return
if (middleResult instanceof Promise) {
middleResult = await middleResult;
}
// ends the response if middleware explicitly returns false, else continue
if (middleResult === false) {
response.end();
return;
}
}
// call API with pre-generated $proxy(inputs)
let result = await api.$proxy(inputs, request);
// let this controller handle all response if "handleResponse" function is available
if (api.onResponse) {
api.onResponse(response, result);
}
else {
this.config.responder.write(result, response);
}
}
catch (err) {
if (err instanceof Error) {
this.logService.error(err);
}
if (!response.headersSent) {
// catch error if responder error happens
try {
this.config.responder.writeError(err, response, this.errorService);
}
catch (err) {
this.logService.error(err);
let error = this.errorService.getError(1001);
response.end(JSON.stringify({ error }));
}
}
}
}
/**
* get request data
*/
getEntityBody(request) {
let contentLenth = parseInt(request.headers['content-length'], 0);
if (Number.isInteger(contentLenth) &&
this.maxContentLength > 0 &&
contentLenth > this.maxContentLength) {
return Promise.reject(new error_1.KiteError(1009, this.config.maxContentLength));
}
return new Promise((resolve, reject) => {
let buffer = [];
// register 'data' event, and push chunks to the buffer
request.on('data', (chunk) => {
// check if post data size exceeds config.maxPostSize
if (this.maxContentLength > 0 && buffer.length > this.maxContentLength) {
return reject(new error_1.KiteError(1009, this.config.maxContentLength));
}
buffer.push(chunk);
});
request.on('end', () => {
resolve(Buffer.concat(buffer).toString());
});
});
}
log(msg, color = '\x1b[35m') {
let time = new Date().toLocaleString();
console.log(color + time, '[ KITE ]', msg, '\x1b[0m');
}
/**
* Add a middleware to Kite
* @param middleware middleware function
*/
use(middleware) {
this.middlewares.add(middleware);
// return `this` for chain
return this;
}
/**
* @since 0.5.7
*
* Start a service in Kite boot / fly stage, call this method to start any number of services
* and inject dependencies when Kite starting, the services will be started immediately
* after calling this method.
*
* eg:
* ```ts
* new Kite().start(Service1, Service2);
* ```
* @param services any number of service classes
*/
start(...services) {
this.controllerFactory.startService(...services);
return this;
}
/**
* Set train kite data provider (scope).
*
* If a provider is given, Kite will invoke the `exec()` method when request comes,
* the return value must be a "injectable" object
* @param provider Provider instance
*/
useProvider(provider) {
this._provider = provider;
return this;
}
/**
* @since 0.5.10
*
* Set kite router
*
* if router is not given, a built-in http router will be used
* @param router A `Router` instance or a function that resolves router
*/
route(router) {
if (typeof router === 'object' && router.map) {
this.router = router;
}
else if (typeof router === 'function') {
this.router = router.call(null);
}
else {
let rootdir = path.join(this.workdir, 'controllers');
this.router = new http_router_1.HttpRouter(rootdir, '.controller.js');
}
return this;
}
}
exports.Kite = Kite;