Skip to content
This repository has been archived by the owner on Aug 16, 2024. It is now read-only.

Commit

Permalink
feat(server): beta.14 - server as injectable (#194)
Browse files Browse the repository at this point in the history
  • Loading branch information
serhiisol authored Aug 26, 2023
1 parent d983552 commit 9e34341
Show file tree
Hide file tree
Showing 15 changed files with 96 additions and 55 deletions.
12 changes: 10 additions & 2 deletions server/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ Fully working example can be found in [example](example) folder.
## Application
In order to create an application, use `Application` class with app root module:
```typescript
const app = await Application.create(AppModule);
const app = await Application.create(AppModule, server?);
```
Application instance provides an `inject` method to retrieve instances of any provided objects:
Expand All @@ -42,6 +42,7 @@ await module.listen(3000);
```typescript
import { Application, Module } from '@decorators/server';
import { HttpModule } from '@decorators/server/http';
import { ExpressAdapter } from '@decorators/server/express';

@Module({
modules: [
Expand All @@ -52,7 +53,14 @@ export class AppModule { }
```
## Adapters
* `ExpressAdapter` - adapter for [express](https://github.com/expressjs/express)
* `ExpressAdapter` - adapter for [express](https://github.com/expressjs/express) from `@decorators/server/express`
* `FastifyAdapter` - adapter for [fastify](https://github.com/fastify/fastify) from `@decorators/server/fastify`
* `KoaAdapter` - adapter for [koa](https://github.com/koajs/koa) from `@decorators/server/koa`
Adapter can be instantiated with existing application (for example express application):
```ts
HttpModule.create(new ExpressAdapter(app));
```
## Payload vaidation
Package supports [class-validator](https://github.com/typestack/class-validator) and [class-transformer](https://github.com/typestack/class-transformer) packages, basic types validation is supported as well:
Expand Down
4 changes: 2 additions & 2 deletions server/package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion server/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -142,5 +142,5 @@
]
}
},
"version": "1.0.0-beta.13"
"version": "1.0.0-beta.14"
}
11 changes: 8 additions & 3 deletions server/src/core/application.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,22 @@
import { InjectionToken, RootContainer } from '@decorators/di';
import { createServer } from 'http';

import { ContainerManager, ModuleResolver, ROOT_MODULE } from './helpers';
import { APP_SERVER, ContainerManager, ModuleResolver, ROOT_MODULE } from './helpers';
import { DEFAULT_PROVIDERS } from './providers';
import { ClassConstructor } from './types';
import { ClassConstructor, Server } from './types';

export class Application {
static async create(rootModule: ClassConstructor) {
static async create(rootModule: ClassConstructor, server?: Server) {
const containerManger = new ContainerManager();
const container = containerManger.create(Application);

container.setParent(RootContainer);

container.provide([
{
provide: APP_SERVER,
useValue: server ?? createServer(),
},
{
provide: ContainerManager,
useValue: containerManger,
Expand Down
2 changes: 2 additions & 0 deletions server/src/core/helpers/constants/injectables.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,3 +11,5 @@ export const ROOT_MODULE_INSTANCE = new InjectionToken('__server__:root-module-i
* Provides global application prefix/version for entire server
*/
export const APP_VERSION = new InjectionToken('__server__:app-version');

export const APP_SERVER = new InjectionToken('__server__');
5 changes: 5 additions & 0 deletions server/src/core/types.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,13 @@
import { Provider } from '@decorators/di';
import type { Server as HttpServer } from 'http';
import type { Http2SecureServer as Http2Server } from 'http2';
import type { Server as HttpsServer } from 'https';

export type ClassConstructor<T = object> = new (...args: any[]) => T;
export type Handler = (...args: unknown[]) => Promise<unknown> | unknown;

export type Server = HttpServer | HttpsServer | Http2Server;

export interface ModuleMetadata {
controllers: ClassConstructor[];
modules: ClassConstructor[];
Expand Down
5 changes: 5 additions & 0 deletions server/src/core/utils.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ export function addLeadingSlash(url: string): string {

export function buildUrl(...paths: string[]) {
return paths
.filter(Boolean)
.map(path => path.startsWith('/') ? path.slice(1) : path)
.filter(Boolean)
.join('/');
Expand Down Expand Up @@ -41,3 +42,7 @@ export function isClass(type: Handler | ClassConstructor) {
export function isFunction(type: Handler | ClassConstructor) {
return typeof type === 'function' && !isClass(type);
}

export function isEnum(type: object, val: unknown) {
return Object.values(type).includes(val);
}
19 changes: 11 additions & 8 deletions server/src/platforms/express/express-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,17 @@
import * as express from 'express';
import { Server } from 'http';

import { HttpApplicationAdapter, ParameterType } from '../http/helpers';
import { Route } from '../http/types';
import { Server } from '../../core';
import { AdapterRoute, HttpApplicationAdapter, ParameterType } from '../http';

export class ExpressAdapter implements HttpApplicationAdapter {
server?: Server;
type = 'express';
private server: Server;

constructor(public app: express.Express = express()) { }
constructor(public app = express()) { }

attachServer(server: Server) {
this.server = server;
}

close() {
this.server?.close();
Expand All @@ -31,8 +34,8 @@ export class ExpressAdapter implements HttpApplicationAdapter {
return response.headersSent;
}

listen(port: number) {
this.server = this.app.listen(port);
listen() {
this.server.on('request', this.app);
}

render(response: express.Response, template: string, message: object) {
Expand All @@ -57,7 +60,7 @@ export class ExpressAdapter implements HttpApplicationAdapter {
return response.send(message);
}

routes(routes: Route[]) {
routes(routes: AdapterRoute[]) {
for (const route of routes) {
this.app[route.type]?.(route.url, route.handler);
}
Expand Down
26 changes: 14 additions & 12 deletions server/src/platforms/fastify/fastify-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,21 @@
import * as FastifyStatic from '@fastify/static';
import * as Fastify from 'fastify';
import { Server } from 'http';

import { HttpApplicationAdapter, ParameterType } from '../http/helpers';
import { Route } from '../http/types';
import { Server } from '../../core';
import { AdapterRoute, HttpApplicationAdapter, ParameterType } from '../http';

export class FastifyAdapter implements HttpApplicationAdapter {
server?: Server;
type = 'fastify';
private server: Server;

constructor(public app: Fastify.FastifyInstance = Fastify()) { }
constructor(public app = Fastify()) { }

close() {
this.server?.close();
attachServer(server: Server) {
this.server = server;
}

async close() {
await this.app.close();
}

getParam(type: ParameterType, name: string, req: Fastify.FastifyRequest, res: Fastify.FastifyReply) {
Expand All @@ -32,10 +35,9 @@ export class FastifyAdapter implements HttpApplicationAdapter {
return response.sent;
}

async listen(port: number) {
await this.app.listen({ port });

this.server = this.app.server;
async listen() {
await this.app.ready();
this.server.on('request', this.app.routing);
}

render(_response: Fastify.FastifyReply, template: string, message: object) {
Expand All @@ -60,7 +62,7 @@ export class FastifyAdapter implements HttpApplicationAdapter {
return response.send(message);
}

routes(routes: Route[]) {
routes(routes: AdapterRoute[]) {
for (const route of routes) {
this.app[route.type]?.(route.url, route.handler);
}
Expand Down
13 changes: 6 additions & 7 deletions server/src/platforms/http/helpers/http-application-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
import { Server } from 'http';

import { Route } from '../types';
import { Server } from '../../../core';
import { AdapterRoute } from '../types';
import { ParameterType } from './constants';

export abstract class HttpApplicationAdapter {
abstract server?: Server;
abstract type: string;
abstract close(): void;
abstract attachServer(server: Server): void;
abstract close(): Promise<void> | void;
abstract getParam(type: ParameterType, name?: string, ...args: any[]): Promise<() => unknown> | (() => unknown);
abstract isHeadersSent(response: unknown): Promise<boolean> | boolean;
abstract listen(port: number): Promise<void> | void;
abstract listen(): Promise<void> | void;
abstract render(response: unknown, template: string, message: unknown): Promise<string> | string;
abstract reply(response: unknown, message: unknown, statusCode?: number): Promise<unknown> | unknown;
abstract routes(routes: Route[]): void;
abstract routes(routes: AdapterRoute[]): void;
abstract serveStatic(prefix: string, path: string, options?: object): void;
abstract set?(setting: string, value: unknown): void;
abstract setHeader(response: unknown, name: string, value: string): void;
Expand Down
8 changes: 5 additions & 3 deletions server/src/platforms/http/helpers/metadata-scanner.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Inject, Injectable, Optional } from '@decorators/di';

import { addLeadingSlash, APP_VERSION, buildUrl, ClassConstructor, MethodMetadata, Reflector, ROOT_MODULE } from '../../../core';
import { addLeadingSlash, APP_VERSION, buildUrl, ClassConstructor, isEnum, MethodMetadata, Reflector, ROOT_MODULE } from '../../../core';
import { RouteMetadata } from '../types';
import { METHOD_TEMPLATE_METADATA } from './constants';
import { HttpMethodType, METHOD_TEMPLATE_METADATA } from './constants';

@Injectable()
export class MetadataScanner {
Expand All @@ -21,8 +21,10 @@ export class MetadataScanner {

const routes = controllers.map(controller => {
const metadata = this.reflector.getControllerMetadata(controller);
const methods = metadata.methods
.filter((method: MethodMetadata) => isEnum(HttpMethodType, method.type));

return metadata.methods.map((method: MethodMetadata) => {
return methods.map((method: MethodMetadata) => {
const template = this.reflector.getMetadata(METHOD_TEMPLATE_METADATA, controller.prototype[method.methodName]) as string;
const params = metadata.params.filter(param => param.methodName === method.methodName);
const pipes = metadata.pipes
Expand Down
21 changes: 14 additions & 7 deletions server/src/platforms/http/http.module.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { Inject } from '@decorators/di';

import { ClassConstructor, Module, ModuleWithProviders } from '../../core';
import { APP_SERVER, ClassConstructor, Module, ModuleWithProviders, Server } from '../../core';
import { HTTP_ADAPTER, HttpApplicationAdapter, MetadataScanner, RouteHandler, RouteResolver } from './helpers';

@Module({
Expand All @@ -11,33 +11,40 @@ import { HTTP_ADAPTER, HttpApplicationAdapter, MetadataScanner, RouteHandler, Ro
],
})
export class HttpModule {
static create(adapter: ClassConstructor<HttpApplicationAdapter>) {
static create(
adapter: ClassConstructor<HttpApplicationAdapter> | InstanceType<ClassConstructor<HttpApplicationAdapter>>,
) {
return {
module: HttpModule,
providers: [{
provide: HTTP_ADAPTER,
useClass: adapter,
...(adapter instanceof HttpApplicationAdapter ? { useValue: adapter } : { useClass: adapter }),
}],
} as ModuleWithProviders;
}

constructor(
@Inject(APP_SERVER) private server: Server,
@Inject(HTTP_ADAPTER) private adapter: HttpApplicationAdapter,
private routeResolver: RouteResolver,
) { }
) {
this.adapter.attachServer(this.server);
}

close() {
this.adapter.close();
return this.adapter.close();
}

getHttpServer() {
return this.adapter.server;
return this.server;
}

async listen(port?: number) {
await this.routeResolver.resolve();

return this.adapter.listen(port);
await this.adapter.listen();

return this.server.listen(port);
}

set(setting: string, value: unknown) {
Expand Down
2 changes: 1 addition & 1 deletion server/src/platforms/http/index.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
export * from './decorators';
export { HTTP_ADAPTER, HttpApplicationAdapter, HttpContext, MetadataScanner, ParameterType } from './helpers';
export * from './http.module';
export { RouteMetadata } from './types';
export { AdapterRoute, RouteMetadata } from './types';
2 changes: 1 addition & 1 deletion server/src/platforms/http/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ export interface RouteMetadata extends MethodMetadata {
template?: string;
}

export interface Route {
export interface AdapterRoute {
handler: Handler;
type: string;
url: string;
Expand Down
19 changes: 11 additions & 8 deletions server/src/platforms/koa/koa-adapter.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,20 @@
import { Server } from 'http';
import * as Koa from 'koa';
import * as koaMount from 'koa-mount';
import * as KoaRouter from 'koa-router';
import * as koaStatic from 'koa-static';

import { HttpApplicationAdapter, ParameterType } from '../http/helpers';
import { Route } from '../http/types';
import { Server } from '../../core';
import { AdapterRoute, HttpApplicationAdapter, ParameterType } from '../http';

export class KoaAdapter implements HttpApplicationAdapter {
server?: Server;
type = 'koa';
private server: Server;

constructor(public app: Koa = new Koa()) { }
constructor(public app = new Koa()) { }

attachServer(server: Server): void {
this.server = server;
}

close() {
this.server?.close();
Expand All @@ -37,8 +40,8 @@ export class KoaAdapter implements HttpApplicationAdapter {
return response.headerSent;
}

listen(port: number) {
this.server = this.app.listen(port);
listen() {
this.server.on('request', this.app.callback());
}

async render(response: Koa.Response, template: string, message: object) {
Expand All @@ -65,7 +68,7 @@ export class KoaAdapter implements HttpApplicationAdapter {
response.ctx.body = message;
}

routes(routes: Route[]) {
routes(routes: AdapterRoute[]) {
const router = new KoaRouter();

for (const route of routes) {
Expand Down

0 comments on commit 9e34341

Please sign in to comment.