diff --git a/package.json b/package.json index ffc9a97..ff91b78 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@onixjs/core", - "version": "1.0.0-alpha.8", + "version": "1.0.0-alpha.9", "description": "The High-Performance SOA Real-Time Framework for Node.JS", "main": "dist/src/index.js", "scripts": { @@ -47,8 +47,8 @@ "node": ">=8.10.0" }, "dependencies": { + "@onixjs/sdk": "^1.0.0-alpha.4.4", "reflect-metadata": "^0.1.12", - "@onixjs/sdk": "^1.0.0-alpha.4.1", "uws": "^9.14.0" }, "devDependencies": { diff --git a/src/core/app.server.ts b/src/core/app.server.ts index 841e3ea..2d6f330 100644 --- a/src/core/app.server.ts +++ b/src/core/app.server.ts @@ -72,6 +72,9 @@ export class AppServer { switch (operation.type) { // Event sent from broker when loading a project case OperationType.APP_CREATE: + // Use Host Level configurations, like custom ports + Object.assign(this.config, operation.message); + // Setup factory, responser and streamer this.factory = new AppFactory(this.AppClass, this.config); this.responser = new CallResponser(this.factory, this.AppClass); this.streamer = new CallStreamer(this.factory, this.AppClass); diff --git a/src/core/http.server.ts b/src/core/http.server.ts index 4c3a6d6..a723769 100644 --- a/src/core/http.server.ts +++ b/src/core/http.server.ts @@ -1,11 +1,16 @@ import 'reflect-metadata'; import {OnixConfig} from '../index'; -import {EndpointDirectory, HttpRequestHandler} from '../interfaces'; +import { + HttpRequestHandler, + HTTPMethodsDirectory, + HTTPMethods, +} from '../interfaces'; import * as fs from 'fs'; import * as url from 'url'; import * as http from 'http'; import * as https from 'https'; import * as querystring from 'querystring'; +import {Utils} from '@onixjs/sdk/dist/utils'; /** * @function HTTPServer * @author Jonathan Casarrubias @@ -26,7 +31,14 @@ export class HTTPServer { * @description Will contain a directory of handlers * for specific http request calls. */ - private endpoints: EndpointDirectory = {}; + private endpoints: HTTPMethodsDirectory = { + get: {}, + post: {}, + patch: {}, + put: {}, + update: {}, + delete: {}, + }; /** * @constructor * @param config @@ -91,55 +103,109 @@ export class HTTPServer { stop(): void { this.server.close(); } - + /** + * @method listener + * @param req + * @param res + * @description This method wraps endpoints. This provides framework level + * functionalities to each of the calls. + * + * Example, parse POST or Query. + */ private async listener(req: http.IncomingMessage, res: http.ServerResponse) { - if (req.url) { - // TODO Create HTTP Request that extends from http.IncomingMessage - if (req.method === 'POST') { - req['post'] = await this.processPost(req, res); - } - // Get Query String - req['query'] = querystring.parse(url.parse(req.url).query || ''); - // Define Endpoint - const endpoint: string | undefined = url.parse(req.url).pathname; - //const query: string | null = url.parse(req.url).query; - if (endpoint && this.endpoints[endpoint]) { - this.endpoints[endpoint](req, res); - } else if (this.endpoints['*']) { - this.endpoints['*'](req, res); - } else { - res.end( - JSON.stringify({ - error: { - code: 404, - message: `Unable to process endpoint, missing listener ${endpoint}`, - }, - }), - ); + if (req.url && req.method) { + // Get directory of endpoints for this method + const directory = this.endpoints[req.method.toLowerCase()]; + // Verify we actually got a directory + if (directory) { + // TODO Create HTTP Request that extends from http.IncomingMessage + if (req.method === 'POST') { + req['post'] = await this.processPost(req, res); + req['post'] = Utils.IsJsonString(req['post']) + ? JSON.parse(req['post']) + : req['post']; + } + // Get Query String + req['query'] = querystring.parse(url.parse(req.url).query || ''); + // Define Endpoint + const endpoint: string | undefined = url.parse(req.url).pathname; + //const query: string | null = url.parse(req.url).query; + if (endpoint && directory[endpoint]) { + directory[endpoint](req, res); + } else if (directory['*']) { + directory['*'](req, res); + } else { + res.end( + JSON.stringify({ + error: { + code: 404, + message: `Unable to process endpoint, missing listener ${endpoint}`, + }, + }), + ); + } } - } else { - throw new Error('Missing request.url from http.server'); } } - - register(endpoint: string, handler: HttpRequestHandler): void { - this.endpoints[endpoint] = handler; + /** + * @method register + * @param method + * @param endpoint + * @param handler + * @description This method will register middleware endpoints + * It should be used before the server is started. + */ + register( + method: + | HTTPMethods.GET + | HTTPMethods.POST + | HTTPMethods.PUT + | HTTPMethods.PATCH + | HTTPMethods.DELETE, + endpoint: string, + handler: HttpRequestHandler, + ): void { + switch (method) { + // Using enum instead of string to make this a strict feature + case HTTPMethods.GET: + this.endpoints.get[endpoint] = handler; + break; + case HTTPMethods.POST: + this.endpoints.post[endpoint] = handler; + break; + case HTTPMethods.PUT: + this.endpoints.put[endpoint] = handler; + break; + case HTTPMethods.PATCH: + this.endpoints.patch[endpoint] = handler; + break; + case HTTPMethods.DELETE: + this.endpoints.delete[endpoint] = handler; + break; + } } - + /** + * @method processPost + * @param req + * @param res + * @description Built-in middleware that parses a post data + * and returns a parsed object. + */ private async processPost(req, res): Promise { return new Promise((resolve, reject) => { let data: string = ''; req.on('data', d => { data += d; - // Oops way to large body, kill this guy now + /* Oops way to large body, kill this guy now + Temporally disabled since it is not currently public feature if (data.length > 1e6) { data = ''; res.writeHead(413, {'Content-Type': 'text/plain'}).end(); req.connection.destroy(); - } + }*/ }); req.on('end', function() { - resolve(querystring.parse(data)); + resolve(data); }); }); } diff --git a/src/core/schema.provider.ts b/src/core/schema.provider.ts index 7363160..88d5e0f 100644 --- a/src/core/schema.provider.ts +++ b/src/core/schema.provider.ts @@ -1,5 +1,5 @@ import 'reflect-metadata'; -import {OnixJS, IAppDirectory, IAppConfig} from '../index'; +import {OnixJS, IAppDirectory, IAppConfig, HTTPMethods} from '../index'; import * as http from 'http'; import {HTTPServer} from './http.server'; /** @@ -33,7 +33,9 @@ export class SchemaProvider { start(): void { // Setup server this.server = new HTTPServer(this.onix.config); - this.server.register('/', (req, res) => this.listener(req, res)); + this.server.register(HTTPMethods.GET, '/', (req, res) => + this.listener(req, res), + ); this.server.start(); // Indicate the ONIX SERVER is now listening on the given port console.log( diff --git a/src/index.ts b/src/index.ts index 65c9d7c..9dca8d6 100644 --- a/src/index.ts +++ b/src/index.ts @@ -33,7 +33,7 @@ export class OnixJS { * @description Current Onix Version. */ get version(): string { - return '1.0.0-alpha.8'; + return '1.0.0-alpha.9'; } /** * @property server @@ -131,7 +131,19 @@ export class OnixJS { return new Promise((resolve, reject) => { const parts: string[] = namespace.split('@'); const name: string = parts.shift() || ''; - const directory: string = parts.shift() || ''; + let directory: string = parts.shift() || ''; + let port: number = 0; + let disableNetwork; + if (directory.match(/:[\d]{2,5}/)) { + const p = directory.split(':'); + directory = p.shift() || ''; + port = parseInt(p.shift() || '') || port; + } + if (directory.match(/:disabled/)) { + const p = directory.split(':'); + directory = p.shift() || ''; + disableNetwork = true; + } // Verify for duplicated applications if (this._apps[name]) { reject(new Error('OnixJS Error: Trying to add duplicated application')); @@ -155,6 +167,11 @@ export class OnixJS { // Must Follow App Operation this._apps[name].process.send({ type: OperationType.APP_CREATE, + // Send host level parameters here + message: { + port, + disableNetwork, + }, }); } }); diff --git a/src/interfaces/index.ts b/src/interfaces/index.ts index 2c5b689..4bfdf3f 100644 --- a/src/interfaces/index.ts +++ b/src/interfaces/index.ts @@ -280,9 +280,13 @@ export interface BootConfig { apps: string[]; identityProvider?: DomainConfig; } - -export interface EndpointDirectory { - [key: string]: HttpRequestHandler; +export interface HTTPMethodsDirectory { + get: {[key: string]: HttpRequestHandler}; + post: {[key: string]: HttpRequestHandler}; + patch: {[key: string]: HttpRequestHandler}; + put: {[key: string]: HttpRequestHandler}; + update: {[key: string]: HttpRequestHandler}; + delete: {[key: string]: HttpRequestHandler}; } export interface HttpRequestHandler { (req: http.IncomingMessage, res: http.ServerResponse): void; @@ -363,3 +367,11 @@ export enum ReflectionKeys { /*15*/ INJECTABLE_SERVICE, /*16*/ INJECTABLE_DATASOURCE, } + +export enum HTTPMethods { + GET, + POST, + PATCH, + PUT, + DELETE, +} diff --git a/test/onix.acceptance.ts b/test/onix.acceptance.ts index 26742a3..9e48471 100644 --- a/test/onix.acceptance.ts +++ b/test/onix.acceptance.ts @@ -21,7 +21,7 @@ test('Onix version', t => { **/ test('Onix app starter', async t => { const onix: OnixJS = new OnixJS({cwd, port: 8083}); - await onix.load('TodoApp@todo2.app'); + await onix.load('TodoApp@todo.app:disabled'); const results: OperationType.APP_START_RESPONSE[] = await onix.start(); t.deepEqual(results, [ // One for the server @@ -36,7 +36,7 @@ test('Onix app starter', async t => { */ test('Onix app pinger', async t => { const onix: OnixJS = new OnixJS({cwd, port: 8084}); - await onix.load('TodoApp@todo2.app'); + await onix.load('TodoApp@todo.app:disabled'); const config: IAppConfig = await onix.ping('TodoApp'); t.true(config.disableNetwork); }); @@ -60,7 +60,7 @@ test('Onix app greeter', async t => { **/ test('Onix rpc component methods from server', async t => { const onix: OnixJS = new OnixJS({cwd, port: 8085}); - await onix.load('TodoApp@todo2.app'); + await onix.load('TodoApp@todo.app:disabled'); await onix.start(); const todo: TodoModel = new TodoModel(); todo.text = 'Hello World'; @@ -125,8 +125,10 @@ test('Onix rpc component methods from client', async t => { ***/ test('Onix rpc component stream', async t => { const text: string = 'Hello SDK World'; + // Host Port 8087 const onix: OnixJS = new OnixJS({cwd, port: 8087}); - await onix.load('TodoApp@todo3.app'); + // SOA Service Port 8078 + await onix.load('TodoApp@todo.app:8078'); await onix.start(); // Websocket should be available now const client: OnixClient = new OnixClient({ diff --git a/test/onixjs.core.unit.ts b/test/onixjs.core.unit.ts index 6df0172..6d02b49 100644 --- a/test/onixjs.core.unit.ts +++ b/test/onixjs.core.unit.ts @@ -22,11 +22,17 @@ import { IModel, Model, Property, + LifeCycle, + IApp, + IModuleDirectory, + HTTPServer, + HTTPMethods, } from '../src'; import * as path from 'path'; import {CallResponser} from '../src/core/call.responser'; import * as WebSocket from 'uws'; import {CallStreamer} from '../src/core/call.streamer'; +import {NodeJS} from '@onixjs/sdk/dist/core/node.adapters'; import {Utils} from '@onixjs/sdk/dist/utils'; import {Mongoose, Schema, Model as MongooseModel} from 'mongoose'; const cwd = path.join(process.cwd(), 'dist', 'test'); @@ -59,8 +65,8 @@ test('Core: OnixJS loads creates duplicated Application.', async t => { cwd: path.join(process.cwd(), 'dist', 'test'), port: 8086, }); - await onix.load('TodoApp@todo2.app'); - const error = await t.throws(onix.load('TodoApp@todo2.app')); + await onix.load('TodoApp@todo.app:disabled'); + const error = await t.throws(onix.load('TodoApp@todo.app:disabled')); t.is(error.message, 'OnixJS Error: Trying to add duplicated application'); }); // Test OnixJS ping missing app @@ -92,7 +98,7 @@ test('Core: OnixJS gets list of apps.', async t => { cwd: path.join(process.cwd(), 'dist', 'test'), port: 8089, }); - await onix.load('TodoApp@todo2.app'); + await onix.load('TodoApp@todo.app:disabled'); const apps = onix.apps(); t.is(Object.keys(apps).length, 1); }); @@ -458,7 +464,7 @@ test('Core: Connection.', async t => { test('Core: host boot.', async t => { const host: HostBoot = new HostBoot( { - apps: ['TodoApp@todo4.app'], + apps: ['TodoApp@todo.app:8076'], }, {cwd}, ); @@ -580,3 +586,132 @@ test('Core: Inject Model and Services.', async t => { t.truthy(result._id); t.is(result.text, criteria); }); +// Test Main Life Cycle +test('Core: main lifecycle.', async t => { + const result: boolean = true; + class MyApp implements IApp { + modules: IModuleDirectory; + async start(): Promise { + return result; + } + async stop(): Promise { + return result; + } + isAlive(): boolean { + return result; + } + } + const instance: MyApp = new MyApp(); + const lifecycle: LifeCycle = new LifeCycle(); + const r1 = await lifecycle.onAppMethodCall( + instance, + { + uuid: '1', + rpc: 'somerpc', + request: { + metadata: {}, + payload: {}, + }, + }, + async () => result, + ); + const r2 = await lifecycle.onModuleMethodCall( + instance, + { + uuid: '2', + rpc: 'somerpc', + request: { + metadata: {}, + payload: {}, + }, + }, + async () => result, + ); + const r3 = await lifecycle.onComponentMethodCall( + instance, + { + uuid: '3', + rpc: 'somerpc', + request: { + metadata: {}, + payload: {}, + }, + }, + async () => result, + ); + t.true(r1); + t.true(r2); + t.true(r3); +}); +// Test HTTP Methods +test('Core: HTTP Methods.', async t => { + interface Result { + hello: string; + } + interface HTTPError { + error: { + code: number; + }; + } + const result: Result = {hello: 'world'}; + const config = { + host: '127.0.0.1', + port: 8060, + path: '/hello-post', + }; + const server: HTTPServer = new HTTPServer(config); + // Register middlewares + server.register(HTTPMethods.GET, '/hello-get', (req, res) => { + res.end(JSON.stringify(result)); + }); + server.register(HTTPMethods.POST, config.path, (req, res) => { + res.end(JSON.stringify(req['post'])); + }); + server.register(HTTPMethods.PATCH, '/hello-patch', (req, res) => { + res.end(JSON.stringify(result)); + }); + server.register(HTTPMethods.PUT, '/hello-put', (req, res) => { + res.end(JSON.stringify(result)); + }); + server.register(HTTPMethods.DELETE, '/hello-delete', (req, res) => { + res.end(JSON.stringify(result)); + }); + server.start(); + // Use SDK Client to make calls + const client: NodeJS.HTTP = new NodeJS.HTTP(); + const getResult: Result = await client.get( + `http://${config.host}:${config.port}/hello-get`, + ); + const getError: HTTPError = await client.get( + `http://${config.host}:${config.port}/noexist`, + ); + const postResult: Result = await client.post(config, result); + server.stop(); + t.is(getResult.hello, result.hello); + t.is(getError.error.code, 404); + t.is(postResult.hello, result.hello); +}); +// Test HTTP WildCard +test('Core: HTTP WildCard.', async t => { + interface Result { + hello: string; + } + const result: Result = {hello: 'world'}; + const config = { + host: '127.0.0.1', + port: 8061, + }; + const server: HTTPServer = new HTTPServer(config); + // Register Wildcard middleware + server.register(HTTPMethods.GET, '*', (req, res) => { + res.end(JSON.stringify(result)); + }); + server.start(); + // Use SDK Client to make calls + const client: NodeJS.HTTP = new NodeJS.HTTP(); + const getResult: Result = await client.get( + `http://${config.host}:${config.port}/noexist`, + ); + server.stop(); + t.is(getResult.hello, result.hello); +}); diff --git a/test/todo2.app/index.ts b/test/todo2.app/index.ts deleted file mode 100644 index 6b0db6b..0000000 --- a/test/todo2.app/index.ts +++ /dev/null @@ -1,14 +0,0 @@ -import {TodoModule} from '../todo.shared/todo.module'; -import {MicroService, Application} from '../../src/index'; -/** - * @class TodoApp - * @author Jonathan Casarrubias - * @license MIT - * @description This example app is used as example - * and for testing purposes. It imports a TodoModule. - */ -@MicroService({ - modules: [TodoModule], - disableNetwork: true, -}) -export class TodoApp extends Application {} diff --git a/test/todo3.app/index.ts b/test/todo3.app/index.ts deleted file mode 100644 index 3eadac6..0000000 --- a/test/todo3.app/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {TodoModule} from '../todo.shared/todo.module'; -import {MicroService, Application} from '../../src/index'; -/** - * @class TodoApp - * @author Jonathan Casarrubias - * @license MIT - * @description This example app is used as example - * and for testing purposes. It imports a TodoModule. - */ -@MicroService({ - host: '127.0.0.1', - port: 8078, - modules: [TodoModule], -}) -export class TodoApp extends Application {} diff --git a/test/todo4.app/index.ts b/test/todo4.app/index.ts deleted file mode 100644 index ea7c725..0000000 --- a/test/todo4.app/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import {TodoModule} from '../todo.shared/todo.module'; -import {MicroService, Application} from '../../src/index'; -/** - * @class TodoApp - * @author Jonathan Casarrubias - * @license MIT - * @description This example app is used as example - * and for testing purposes. It imports a TodoModule. - */ -@MicroService({ - host: '127.0.0.1', - port: 8076, - modules: [TodoModule], -}) -export class TodoApp extends Application {}