diff --git a/.gitignore b/.gitignore index 7ebaa75d2..4c0dab10b 100644 --- a/.gitignore +++ b/.gitignore @@ -3,6 +3,10 @@ # IDE /.idea +/.awcache # misc -npm-debug.log \ No newline at end of file +npm-debug.log + +# example +/.example \ No newline at end of file diff --git a/LICENSE b/LICENSE new file mode 100644 index 000000000..be7581ff8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,22 @@ +(The MIT License) + +Copyright (c) 2017 Kamil Myƛliwiec + +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. + +THE SOFTWARE IS PROVIDED 'AS IS', WITHOUT WARRANTY OF ANY KIND, +EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF +MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. +IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY +CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, +TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE +SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. diff --git a/Readme.md b/Readme.md new file mode 100644 index 000000000..5ab41d5ef --- /dev/null +++ b/Readme.md @@ -0,0 +1,27 @@ +[![Nest Logo](http://kamilmysliwiec.com/public/nest-logo.png)](http://kamilmysliwiec.com/) + + Modern, fast, powerful web framework for [node](http://nodejs.org). + + [![NPM Version][npm-image]][npm-url] + [![NPM Downloads][downloads-image]][downloads-url] + [![Linux Build][travis-image]][travis-url] + [![Windows Build][appveyor-image]][appveyor-url] + +## License + + [MIT](LICENSE) + +[npm-image]: https://img.shields.io/npm/v/nest.js.svg +[npm-url]: https://npmjs.org/package/nest.js +[downloads-image]: https://img.shields.io/npm/dm/nest.js.svg +[downloads-url]: https://npmjs.org/package/nest.js +[travis-image]: https://img.shields.io/travis/nest.jsjs/nest.js/master.svg?label=linux +[travis-url]: https://travis-ci.org/nest.jsjs/nest.js +[appveyor-image]: https://img.shields.io/appveyor/ci/dougwilson/nest.js/master.svg?label=windows +[appveyor-url]: https://ci.appveyor.com/project/dougwilson/nest.js +[coveralls-image]: https://img.shields.io/coveralls/nest.jsjs/nest.js/master.svg +[coveralls-url]: https://coveralls.io/r/nest.jsjs/nest.js?branch=master +[gratipay-image-visionmedia]: https://img.shields.io/gratipay/visionmedia.svg +[gratipay-url-visionmedia]: https://gratipay.com/visionmedia/ +[gratipay-image-dougwilson]: https://img.shields.io/gratipay/dougwilson.svg +[gratipay-url-dougwilson]: https://gratipay.com/dougwilson/ diff --git a/example/app.ts b/example/app.ts new file mode 100644 index 000000000..49d1ad2b2 --- /dev/null +++ b/example/app.ts @@ -0,0 +1,19 @@ +import { ExpressConfig } from "./config"; +import { PassportJWTConfig } from "./config/passport-jwt.config"; +import { NestApplication } from "./../src/"; + +export class Application implements NestApplication { + + constructor(private app) { + ExpressConfig.setupConfig(this.app); + PassportJWTConfig.setupConfig(this.app); + } + + public start() { + console.log("star t"); + this.app.listen(3030, () => { + console.log("Nest Application listen on port:", 3030); + }); + } + +} \ No newline at end of file diff --git a/example/config/app.config.ts b/example/config/app.config.ts new file mode 100644 index 000000000..41f3b69fa --- /dev/null +++ b/example/config/app.config.ts @@ -0,0 +1,3 @@ +export class ApplicationConfig { + +} \ No newline at end of file diff --git a/example/config/express.config.ts b/example/config/express.config.ts new file mode 100644 index 000000000..f16c9ad87 --- /dev/null +++ b/example/config/express.config.ts @@ -0,0 +1,21 @@ +import { Application } from "express"; +import * as bodyParser from "body-parser"; +import * as logger from "morgan"; + +export class ExpressConfig { + + static setupConfig(app: Application) { + app.use(logger("dev")); + app.use(bodyParser.json()); + app.use(bodyParser.urlencoded({ extended: true })); + + // Add headers + app.use(function (req, res, next) { + res.setHeader('Access-Control-Allow-Origin', 'http://localhost:4200'); + res.setHeader('Access-Control-Allow-Methods', 'GET, POST, OPTIONS, PUT, PATCH, DELETE'); + res.setHeader('Access-Control-Allow-Headers', 'X-Requested-With,content-type'); + next(); + }); + } + +} \ No newline at end of file diff --git a/example/config/index.ts b/example/config/index.ts new file mode 100644 index 000000000..030da0eca --- /dev/null +++ b/example/config/index.ts @@ -0,0 +1,3 @@ +export * from "./app.config"; +export * from "./express.config"; +export * from "./passport-jwt.config"; \ No newline at end of file diff --git a/example/config/passport-jwt.config.ts b/example/config/passport-jwt.config.ts new file mode 100644 index 000000000..ff9d9cfe4 --- /dev/null +++ b/example/config/passport-jwt.config.ts @@ -0,0 +1,45 @@ +import * as _ from "lodash"; +import { Application } from "express"; +import * as passport from "passport"; +import { ExtractJwt, Strategy, StrategyOptions } from "passport-jwt"; + +var users = [ + { + id: 1, + name: 'jonathanmh', + password: '%2yx4' + }, + { + id: 2, + name: 'test', + password: 'test' + } +]; + +export class PassportJWTConfig { + static readonly secretKey = "XD"; + + static readonly jwtOptions: StrategyOptions = { + jwtFromRequest: ExtractJwt.fromAuthHeader(), + secretOrKey: PassportJWTConfig.secretKey, + }; + + static setupConfig(app: Application) { + this.init(); + app.use(passport.initialize()); + } + + static init() { + + var strategy = new Strategy(this.jwtOptions, (payload, next) => { + console.log('payload received', payload); + + var user = users[_.findIndex(users, {id: payload.id})]; + console.log(user); + next(null, user || false); + }); + + passport.use(strategy); + } + +} \ No newline at end of file diff --git a/example/models/db.ts b/example/models/db.ts new file mode 100644 index 000000000..06033e7c7 --- /dev/null +++ b/example/models/db.ts @@ -0,0 +1,10 @@ +import * as mongoose from "mongoose"; +mongoose.connect("mongodb://localhost:27017/tracker", { + server: { + socketOptions: { + socketTimeoutMS: 0, + connectTimeoutMS: 0 + } + } +}); +export { mongoose as db }; \ No newline at end of file diff --git a/example/models/user.ts b/example/models/user.ts new file mode 100644 index 000000000..a3ac61e7c --- /dev/null +++ b/example/models/user.ts @@ -0,0 +1,13 @@ +import * as mongoose from "mongoose"; +import { db } from "./db"; + +interface IUser extends mongoose.Document { + name: string; + heh: any; +} + +type UserType = IUser & mongoose.Document; + +export const User = db.model('User', new mongoose.Schema({ + name : {type : String, required : true}, +})); diff --git a/example/modules/app.module.ts b/example/modules/app.module.ts new file mode 100644 index 000000000..3441e8a8e --- /dev/null +++ b/example/modules/app.module.ts @@ -0,0 +1,13 @@ +import { UsersModule } from "./users/users.module"; +import { AuthModule } from "./auth/auth.module"; +import { Module } from "./../../src/"; + +@Module({ + modules: [ + UsersModule, + AuthModule + ] +}) +export class ApplicationModule { + configure() {} +} \ No newline at end of file diff --git a/example/modules/auth/auth.module.ts b/example/modules/auth/auth.module.ts new file mode 100644 index 000000000..55f63f858 --- /dev/null +++ b/example/modules/auth/auth.module.ts @@ -0,0 +1,19 @@ +import { AuthRoute } from "./login.route"; +import { SharedModule } from "../shared.module"; +import { Module } from "./../../../src/"; + +@Module({ + modules: [ + SharedModule, + ], + controllers: [ + AuthRoute + ], + components: [ + ] +}) +export class AuthModule { + configure() { + console.log("auth configured"); + } +} \ No newline at end of file diff --git a/example/modules/auth/login.route.ts b/example/modules/auth/login.route.ts new file mode 100644 index 000000000..ea27bf698 --- /dev/null +++ b/example/modules/auth/login.route.ts @@ -0,0 +1,45 @@ +import * as _ from "lodash"; +import * as jwt from "jsonwebtoken"; +import { Request, Response, NextFunction } from "express"; +import { PassportJWTConfig } from "../../config/passport-jwt.config"; +import { Controller, RequestMethod, RequestMapping } from "./../../../src/"; + +var users = [ + { + id: 1, + name: 'jonathanmh', + password: '%2yx4' + }, + { + id: 2, + name: 'test', + password: 'test' + } +]; + +@Controller({ path: "login" }) +export class AuthRoute { + + constructor() {} + + @RequestMapping({ path: "/", method: RequestMethod.POST }) + fetchToken(req: Request, res: Response, next: NextFunction) { + const { name, password } = req.body; + + const user = users[_.findIndex(users, {name: name})]; + if (!user){ + res.status(401).json({message:"no such user found"}); + } + + if(user.password === password) { + const payload = {id: user.id}; + const token = jwt.sign(payload, PassportJWTConfig.secretKey); + res.json({message: "ok", token: token}); + } + else { + res.status(401).json({message:"passwords did not match"}); + } + } + +} + diff --git a/example/modules/shared.module.ts b/example/modules/shared.module.ts new file mode 100644 index 000000000..223e37098 --- /dev/null +++ b/example/modules/shared.module.ts @@ -0,0 +1,15 @@ + +import { SharedService } from "./shared.service"; +import { Module } from "./../../src/"; + +@Module({ + components: [ + SharedService, + ], + exports: [ + SharedService, + ] +}) +export class SharedModule { + configure() {} +} \ No newline at end of file diff --git a/example/modules/shared.service.ts b/example/modules/shared.service.ts new file mode 100644 index 000000000..ab9fe387c --- /dev/null +++ b/example/modules/shared.service.ts @@ -0,0 +1,12 @@ +import { Component } from "./../../src/"; +import { Subject } from "rxjs"; + +@Component() +export class SharedService { + public stream$ = new Subject(); + constructor() { + setTimeout(() => { + this.stream$.next("XDDD"); + }, 3000); + } +} \ No newline at end of file diff --git a/example/modules/users/users-query.service.ts b/example/modules/users/users-query.service.ts new file mode 100644 index 000000000..977212428 --- /dev/null +++ b/example/modules/users/users-query.service.ts @@ -0,0 +1,34 @@ +//import { User } from "../../models/user"; +import { SharedService } from "../shared.service"; +import { Component, RequestMapping } from "./../../../src/"; +import { UsersGateway } from "./users.gateway"; + +import { Subject } from "rxjs"; + +@Component() +export class UsersQueryService { + + public stream$ = new Subject(); + static get dependencies() { + return [ UsersGateway, SharedService ]; + } + + constructor(private usersGateway: UsersGateway, private shared: SharedService) { + this.shared.stream$.subscribe((xd) => { + console.log("ONCE"); + this.stream$.next(xd); + }); + } + /* + getAllUsers(): Promise { + return new Promise((resolve) => { + User.find((err, res) => { + if(err) { + throw new Error(err); + } + + resolve(res); + }); + }); + }*/ +} \ No newline at end of file diff --git a/example/modules/users/users-sec.route.ts b/example/modules/users/users-sec.route.ts new file mode 100644 index 000000000..a3c6c0140 --- /dev/null +++ b/example/modules/users/users-sec.route.ts @@ -0,0 +1,31 @@ +import { Request, Response, NextFunction } from "express"; +import { RequestMapping, RequestMethod} from "./../../../src/"; +import { UsersQueryService } from "./users-query.service"; +import { Controller } from "./../../../src/"; + +@Controller({ path: "users" }) +export class UsersSecRoute { + + constructor(private usersQueryService: UsersQueryService) { + this.usersQueryService.stream$.subscribe((xd) => { + console.log(xd); + }); + } + + /*@RequestMapping({ + path: "/", + method: RequestMethod.GET + }) + async getAllUsers(req: Request, res: Response, next: NextFunction) { + try { + const users = await this.usersQueryService.getAllUsers(); + res.status(201).json(users); + next(); + } + catch(e) { + next(e.message); + } + }*/ + +} + diff --git a/example/modules/users/users.gateway.ts b/example/modules/users/users.gateway.ts new file mode 100644 index 000000000..9a6cb3eca --- /dev/null +++ b/example/modules/users/users.gateway.ts @@ -0,0 +1,49 @@ +import { Gateway, SubscribeMessage, SocketServer, SocketGateway } from "./../../../src/socket"; +import { UsersQueryService } from "./users-query.service"; +import { Component } from "./../../../src/"; + +@Component() +@SocketGateway({ namespace: "" }) +export class UsersGateway implements Gateway { + + static get dependencies() { + return [ UsersQueryService ]; + } + + @SocketServer + private server; + + constructor(private queryService: UsersQueryService) { + console.log("Gateway listen"); + this.queryService.stream$.subscribe((xd) => { + console.log(xd); + }); + } + + afterInit(server) { + this.server = server; + console.log("Server initialized"); + } + + handleConnection(client) { + console.log("Client connected"); + setTimeout(() => { + client.emit("msg", { msg: "Hello from the server!" }); + }, 2000); + /* + client.on("msg", (data) => { + console.log(data); + client.emit("msg", { msg: data.msg }); + client.broadcast.emit("msg", { msg: data.msg }); + });*/ + } + + handleDisconnect(client) { + console.log("Client disconnected "); + } + + @SubscribeMessage({ value: "msg" }) + msgHandler(client, data) { + console.log(data); + } +} \ No newline at end of file diff --git a/example/modules/users/users.middleware.ts b/example/modules/users/users.middleware.ts new file mode 100644 index 000000000..ec7604e20 --- /dev/null +++ b/example/modules/users/users.middleware.ts @@ -0,0 +1,43 @@ +import * as jwt from "jsonwebtoken"; +import { UsersQueryService } from "./users-query.service"; +import { Component, Middleware } from "./../../../src/"; + +@Component() +export class JWTMiddleware implements Middleware { + + constructor(private usersQueryService: UsersQueryService) {} + + resolve() { + return (req, res, next) => { + console.log("JWTmiddleware"); + next(); + /*var token = req.body.token || req.query.token || req.headers['x-access-token']; + + if (token) { + + // verifies secret and checks exp + jwt.verify(token, "XD", function(err, decoded) { + if (err) { + return res.json({ success: false, message: 'Failed to authenticate token.' }); + } else { + // if everything is good, save to request for use in other routes + req.decoded = decoded; + next(); + } + }); + + } else { + + // if there is no token + // return an error + return res.status(403).send({ + success: false, + message: 'No token provided.' + }); + + }*/ + } + } + +} + diff --git a/example/modules/users/users.module.ts b/example/modules/users/users.module.ts new file mode 100644 index 000000000..de5d07a55 --- /dev/null +++ b/example/modules/users/users.module.ts @@ -0,0 +1,33 @@ + +import { UsersRoute } from "./users.route"; +import { UsersQueryService } from "./users-query.service"; +import { UsersGateway } from "./users.gateway"; +import { UsersSecRoute } from "./users-sec.route"; +import { JWTMiddleware } from "./users.middleware"; +import { SharedModule } from "../shared.module"; +import { SharedService } from "../shared.service"; +import { Module } from "./../../../src/"; + + @Module({ + modules: [ + SharedModule, + ], + controllers: [ + UsersRoute, + UsersSecRoute + ], + components: [ + UsersQueryService, + UsersGateway, + ] + }) + export class UsersModule { + + configure(router) { + router.use({ + middlewares: [ JWTMiddleware ], + forRoutes: [ UsersRoute, UsersSecRoute ] + }); + } + +} \ No newline at end of file diff --git a/example/modules/users/users.route.ts b/example/modules/users/users.route.ts new file mode 100644 index 000000000..4f89d7f00 --- /dev/null +++ b/example/modules/users/users.route.ts @@ -0,0 +1,69 @@ +import { Request, Response, NextFunction } from "express"; +import { UsersQueryService } from "./users-query.service"; +import { RequestMethod, Controller, Exception, RequestMapping } from "./../../../src/"; + +class NotFoundException extends Exception { + constructor(msg: string) { + super(`Not found ${msg}`, 401); + } +} + +@Controller({ path: "users" }) +export class UsersRoute { + + get dependencies() { + return [ UsersQueryService ]; + } + constructor( + private usersQueryService: UsersQueryService) { + + //console.log(usersQueryService); + this.usersQueryService.stream$.subscribe((xd) => { + ///console.log(xd); + }); + } + + @RequestMapping({ + path: "/", + method: RequestMethod.GET + }) + getAllUsers(req: Request, res: Response, next: NextFunction) { + throw new NotFoundException("user"); + /* try { + //const users = await this.usersQueryService.getAllUsers(); + res.status(201).json({ + data: [ + { name: "Todo 1", status: 3 }, + { name: "Todo 2", status: 0 }, + { name: "Todo 3", status: 2 }, + ], + }); + next(); + } + catch(e) { + next(e.message); + }*/ + } + + @RequestMapping({ + path: "/", + method: RequestMethod.POST + }) + async addTodoItem(req: Request, res: Response) { + try { + console.log(req.body); + res.status(201).json({}); + //const users = await this.usersQueryService.getAllUsers(); + /*res.status(201).json({ + data: [ + { name: "Todo 1", status: 3 }, + { name: "Todo 2", status: 0 }, + { name: "Todo 3", status: 2 }, + ], + });*/ + } + catch(e) {} + } + +} + diff --git a/example/server.ts b/example/server.ts new file mode 100644 index 000000000..b0e1c0348 --- /dev/null +++ b/example/server.ts @@ -0,0 +1,5 @@ +import { NestRunner } from "./../src/"; +import { Application } from "./app"; +import { ApplicationModule } from "./modules/app.module"; + +NestRunner.run(Application, ApplicationModule); \ No newline at end of file diff --git a/index.js b/index.js index 6765c5399..03db2d8ea 100644 --- a/index.js +++ b/index.js @@ -1,2 +1,14 @@ require('ts-node/register'); -require('./src/server'); \ No newline at end of file +require('./example/server'); + + + + + + + + + + + + diff --git a/karma.conf.js b/karma.conf.js new file mode 100644 index 000000000..a85b1b4c1 --- /dev/null +++ b/karma.conf.js @@ -0,0 +1,18 @@ +const webpack = require('webpack'); +const webpackConfig = require('./webpack.config.test'); + +module.exports = function (config) { + config.set({ + browsers: [ 'Chrome' ], + colors: true, + reporters: [ 'mocha' ], + frameworks: [ 'mocha', 'chai', 'sinon' ], + files: [ + 'tests.webpack.js' + ], + preprocessors: { + 'tests.webpack.js': [ 'webpack' ] + }, + webpack: webpackConfig, + }); +}; \ No newline at end of file diff --git a/package.json b/package.json index eb3561263..403157b51 100644 --- a/package.json +++ b/package.json @@ -6,34 +6,46 @@ "scripts": { "start": "npm run build:live", "build:live": "nodemon -e ts --watch src index.js", - "build": "node index.js" + "build": "node index.js", + "compile": "tsc -p tsconfig.prod.json", + "test": "karma start" }, "author": "Kamil Mysliwiec", "license": "ISC", "dependencies": { - "@types/body-parser": "0.0.33", - "@types/express": "^4.0.34", - "@types/jsonwebtoken": "^7.2.0", - "@types/lodash": "^4.14.45", - "@types/mongoose": "^4.7.2", - "@types/passport": "^0.3.2", - "@types/passport-jwt": "^2.0.19", - "body-parser": "^1.15.2", - "express": "^4.14.0", - "jsonwebtoken": "^7.2.1", - "lodash": "^4.17.4", - "mongoose": "^4.7.6", - "passport": "^0.3.2", - "passport-jwt": "^2.2.1", - "reflect-metadata": "^0.1.9", - "rxjs": "^5.0.3", - "socket.io": "^1.7.2", "typescript": "^2.1.4" }, "devDependencies": { - "@types/morgan": "^1.7.32", - "morgan": "^1.7.0", + "@types/chai": "^3.4.34", + "@types/karma": "^0.13.33", + "@types/mocha": "^2.2.38", + "@types/sinon": "^1.16.34", + "awesome-typescript-loader": "^3.0.0-beta.18", + "chai": "^3.5.0", + "core-js": "^2.4.1", + "imports-loader": "^0.7.0", + "json-loader": "^0.5.4", + "karma": "^1.4.0", + "karma-chai": "^0.1.0", + "karma-chrome-launcher": "^2.0.0", + "karma-coverage": "^1.1.1", + "karma-mocha": "^1.3.0", + "karma-mocha-reporter": "^2.2.2", + "karma-sinon": "^1.0.5", + "karma-typescript": "^2.1.7", + "karma-webpack": "^2.0.1", + "mocha": "^3.2.0", "nodemon": "^1.11.0", - "ts-node": "^2.0.0" + "sinon": "^2.0.0-pre.2", + "sinon-chai": "^2.8.0", + "ts-node": "^2.0.0", + "webpack": "^1.14.0" + }, + "peerDependencies": { + "cli-color": "^1.1.0", + "express": "^4.14.0", + "reflect-metadata": "^0.1.9", + "rxjs": "^5.0.3", + "socket.io": "^1.7.2" } } diff --git a/src/nest/core/enums/index.ts b/src/common/enums/index.ts similarity index 100% rename from src/nest/core/enums/index.ts rename to src/common/enums/index.ts diff --git a/src/nest/core/enums/request-method.enum.ts b/src/common/enums/request-method.enum.ts similarity index 90% rename from src/nest/core/enums/request-method.enum.ts rename to src/common/enums/request-method.enum.ts index 9902b975e..189a85a13 100644 --- a/src/nest/core/enums/request-method.enum.ts +++ b/src/common/enums/request-method.enum.ts @@ -3,4 +3,5 @@ export enum RequestMethod { POST, PUT, DELETE, + ALL } \ No newline at end of file diff --git a/src/common/index.ts b/src/common/index.ts new file mode 100644 index 000000000..264a0b921 --- /dev/null +++ b/src/common/index.ts @@ -0,0 +1,3 @@ +export * from './utils'; +export * from './enums'; +export { NestModule, NestApplication } from './interfaces'; \ No newline at end of file diff --git a/src/common/interfaces/controller-metadata.interface.ts b/src/common/interfaces/controller-metadata.interface.ts new file mode 100644 index 000000000..b816bf9aa --- /dev/null +++ b/src/common/interfaces/controller-metadata.interface.ts @@ -0,0 +1,3 @@ +export interface ControllerMetadata { + path?: string; +} diff --git a/src/common/interfaces/controller.interface.ts b/src/common/interfaces/controller.interface.ts new file mode 100644 index 000000000..30fb9c54b --- /dev/null +++ b/src/common/interfaces/controller.interface.ts @@ -0,0 +1 @@ +export interface Controller {} diff --git a/src/common/interfaces/index.ts b/src/common/interfaces/index.ts new file mode 100644 index 000000000..4922e252c --- /dev/null +++ b/src/common/interfaces/index.ts @@ -0,0 +1,8 @@ +export * from "./request-mapping-metadata.interface"; +export * from "./nest-module.interface"; +export * from "./module-metadata.interface"; +export * from "./nest-application.interface"; +export * from "./controller.interface"; +export * from "./injectable.interface"; +export * from "./controller-metadata.interface"; +export * from "./nest-application-factory.interface"; \ No newline at end of file diff --git a/src/common/interfaces/injectable.interface.ts b/src/common/interfaces/injectable.interface.ts new file mode 100644 index 000000000..a9e2b48fa --- /dev/null +++ b/src/common/interfaces/injectable.interface.ts @@ -0,0 +1 @@ +export interface Injectable {} diff --git a/src/common/interfaces/module-metadata.interface.ts b/src/common/interfaces/module-metadata.interface.ts new file mode 100644 index 000000000..2e4a9bd5e --- /dev/null +++ b/src/common/interfaces/module-metadata.interface.ts @@ -0,0 +1,9 @@ +import { NestModule } from "./nest-module.interface"; +import { Controller } from "./controller.interface"; + +export interface ModuleMetadata { + modules?: NestModule[], + components?: any[], + controllers?: Controller[], + exports?: any[], +} diff --git a/src/common/interfaces/nest-application-factory.interface.ts b/src/common/interfaces/nest-application-factory.interface.ts new file mode 100644 index 000000000..594df37a1 --- /dev/null +++ b/src/common/interfaces/nest-application-factory.interface.ts @@ -0,0 +1,5 @@ +import { Application } from "express"; + +export interface NestApplicationFactory { + new (app: Application); +} \ No newline at end of file diff --git a/src/nest/core/interfaces/nest-application.interface.ts b/src/common/interfaces/nest-application.interface.ts similarity index 100% rename from src/nest/core/interfaces/nest-application.interface.ts rename to src/common/interfaces/nest-application.interface.ts diff --git a/src/common/interfaces/nest-module.interface.ts b/src/common/interfaces/nest-module.interface.ts new file mode 100644 index 000000000..94636ee03 --- /dev/null +++ b/src/common/interfaces/nest-module.interface.ts @@ -0,0 +1,5 @@ +import { MiddlewareBuilder } from "../../core/middlewares/builder"; + +export interface NestModule { + configure?: (router: MiddlewareBuilder) => MiddlewareBuilder; +} diff --git a/src/common/interfaces/request-mapping-metadata.interface.ts b/src/common/interfaces/request-mapping-metadata.interface.ts new file mode 100644 index 000000000..bb540cf12 --- /dev/null +++ b/src/common/interfaces/request-mapping-metadata.interface.ts @@ -0,0 +1,6 @@ +import { RequestMethod } from "../enums/request-method.enum"; + +export interface RequestMappingMetadata { + path: string, + method?: RequestMethod +} diff --git a/src/common/test/utils/component.decorator.spec.ts b/src/common/test/utils/component.decorator.spec.ts new file mode 100644 index 000000000..7371dd457 --- /dev/null +++ b/src/common/test/utils/component.decorator.spec.ts @@ -0,0 +1,21 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { Component } from "../../utils/component.decorator"; + +describe('@Injectable', () => { + + @Component() + class TestComponent { + constructor( + param: number, + test: string) {} + } + + it('should decorate type with "design:paramtypes" metadata', () => { + const constructorParams = Reflect.getMetadata('design:paramtypes', TestComponent); + + expect(constructorParams[0]).to.be.eql(Number); + expect(constructorParams[1]).to.be.eql(String); + }); + +}); \ No newline at end of file diff --git a/src/common/test/utils/controller.decorator.spec.ts b/src/common/test/utils/controller.decorator.spec.ts new file mode 100644 index 000000000..426cf2a87 --- /dev/null +++ b/src/common/test/utils/controller.decorator.spec.ts @@ -0,0 +1,31 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { Controller } from "../../utils/controller.decorator"; + +describe('@Controller', () => { + const props = { + path: "test", + }; + + @Controller(props) + class Test {} + + @Controller() + class AnotherTest {} + + it('should decorate type with expected path metadata', () => { + const path = Reflect.getMetadata('path', Test); + expect(path).to.be.eql(props.path); + }); + + it('should set default path when no object passed as param', () => { + const path = Reflect.getMetadata('path', AnotherTest); + expect(path).to.be.eql("/"); + }); + + it('should set default path when empty passed as param', () => { + const path = Reflect.getMetadata('path', AnotherTest); + expect(path).to.be.eql("/"); + }); + +}); \ No newline at end of file diff --git a/src/common/test/utils/module.decorator.spec.ts b/src/common/test/utils/module.decorator.spec.ts new file mode 100644 index 000000000..25c3be042 --- /dev/null +++ b/src/common/test/utils/module.decorator.spec.ts @@ -0,0 +1,38 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { Module } from "../../utils/module.decorator"; +import { InvalidModuleConfigException } from "../../../errors/exceptions/invalid-module-config.exception"; + +describe('@Module', () => { + const moduleProps = { + components: [ "Test" ], + modules: [ "Test" ], + exports: [ "Test" ], + controllers: [ "Test" ] + }; + + @Module(moduleProps) + class TestModule {} + + it('should decorate type with expected module metadata', () => { + const modules = Reflect.getMetadata('modules', TestModule); + const components = Reflect.getMetadata('components', TestModule); + const exports = Reflect.getMetadata('exports', TestModule); + const controllers = Reflect.getMetadata('controllers', TestModule); + + expect(modules).to.be.eql(moduleProps.modules); + expect(components).to.be.eql(moduleProps.components); + expect(controllers).to.be.eql(moduleProps.controllers); + expect(exports).to.be.eql(moduleProps.exports); + }); + + it('should throw exception when module properties are invalid', () => { + const invalidProps = { + ...moduleProps, + test: [] + }; + + expect(Module.bind(null, invalidProps)).to.throw(InvalidModuleConfigException); + }); + +}); \ No newline at end of file diff --git a/src/common/test/utils/request-mapping.decorator.spec.ts b/src/common/test/utils/request-mapping.decorator.spec.ts new file mode 100644 index 000000000..ff2aa358e --- /dev/null +++ b/src/common/test/utils/request-mapping.decorator.spec.ts @@ -0,0 +1,40 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { RequestMapping } from "../../utils/request-mapping.decorator"; +import { RequestMethod } from "../../enums/request-method.enum"; +import { InvalidPathVariableException } from "../../../errors/exceptions/invalid-path-variable.exception"; + +describe('@RequestMapping', () => { + const requestProps = { + path: "test", + method: RequestMethod.ALL + }; + + it('should decorate type with expected request metadata', () => { + class Test { + @RequestMapping(requestProps) + static test() {} + } + + const path = Reflect.getMetadata('path', Test.test); + const method = Reflect.getMetadata('method', Test.test); + + expect(method).to.be.eql(requestProps.method); + expect(path).to.be.eql(requestProps.path); + }); + + it('should set request method on GET by default', () => { + class Test { + @RequestMapping({ path: "" }) + static test() {} + } + + const method = Reflect.getMetadata('method', Test.test); + expect(method).to.be.eql(RequestMethod.GET); + }); + + it('should throw exception when path variable is not set', () => { + expect(RequestMapping.bind(null, {})).throw(InvalidPathVariableException); + }); + +}); \ No newline at end of file diff --git a/src/common/utils/component.decorator.ts b/src/common/utils/component.decorator.ts new file mode 100644 index 000000000..5143c0388 --- /dev/null +++ b/src/common/utils/component.decorator.ts @@ -0,0 +1,3 @@ +export const Component = (): ClassDecorator => { + return (target: Object) => {} +}; \ No newline at end of file diff --git a/src/common/utils/controller.decorator.ts b/src/common/utils/controller.decorator.ts new file mode 100644 index 000000000..783bdd3a3 --- /dev/null +++ b/src/common/utils/controller.decorator.ts @@ -0,0 +1,13 @@ +import "reflect-metadata"; +import { ControllerMetadata } from "../interfaces/controller-metadata.interface"; + +const defaultMetadata = { path: "/" }; + +export const Controller = (metadata: ControllerMetadata = defaultMetadata): ClassDecorator => { + if (typeof metadata.path === "undefined") { + metadata.path = "/"; + } + return (target: Object) => { + Reflect.defineMetadata("path", metadata.path, target); + } +}; \ No newline at end of file diff --git a/src/common/utils/index.ts b/src/common/utils/index.ts new file mode 100644 index 000000000..4f0866846 --- /dev/null +++ b/src/common/utils/index.ts @@ -0,0 +1,4 @@ +export * from "./request-mapping.decorator"; +export * from "./controller.decorator"; +export * from "./component.decorator"; +export * from "./module.decorator"; \ No newline at end of file diff --git a/src/common/utils/module.decorator.ts b/src/common/utils/module.decorator.ts new file mode 100644 index 000000000..b87b3487c --- /dev/null +++ b/src/common/utils/module.decorator.ts @@ -0,0 +1,21 @@ +import "reflect-metadata"; +import { ModuleMetadata } from "../interfaces/module-metadata.interface"; +import { InvalidModuleConfigException } from "../../errors/exceptions/invalid-module-config.exception"; + +export const Module = (props: ModuleMetadata): ClassDecorator => { + const propsKeys = Object.keys(props); + const acceptableParams = [ "modules", "exports", "components", "controllers" ]; + + propsKeys.map((prop) => { + if (acceptableParams.findIndex((param) => param === prop) < 0) { + throw new InvalidModuleConfigException(prop); + } + }); + return (target: Object) => { + for (let property in props) { + if (props.hasOwnProperty(property)) { + Reflect.defineMetadata(property, props[property], target); + } + } + } +}; \ No newline at end of file diff --git a/src/common/utils/request-mapping.decorator.ts b/src/common/utils/request-mapping.decorator.ts new file mode 100644 index 000000000..dd43f4fde --- /dev/null +++ b/src/common/utils/request-mapping.decorator.ts @@ -0,0 +1,18 @@ +import "reflect-metadata"; +import { RequestMappingMetadata } from "../interfaces/request-mapping-metadata.interface"; +import { RequestMethod } from "../enums/request-method.enum"; +import { InvalidPathVariableException } from "../../errors/exceptions/invalid-path-variable.exception"; + +export const RequestMapping = (metadata: RequestMappingMetadata): MethodDecorator => { + if (typeof metadata.path === "undefined") { + throw new InvalidPathVariableException("RequestMapping") + } + const requestMethod = metadata.method || RequestMethod.GET; + + return function(target, key, descriptor: PropertyDescriptor) { + Reflect.defineMetadata("path", metadata.path, descriptor.value); + Reflect.defineMetadata("method", requestMethod, descriptor.value); + + return descriptor; + } +}; \ No newline at end of file diff --git a/src/core/adapters/express-adapter.ts b/src/core/adapters/express-adapter.ts new file mode 100644 index 000000000..cdc11c07d --- /dev/null +++ b/src/core/adapters/express-adapter.ts @@ -0,0 +1,12 @@ +import * as express from "express"; + +export class ExpressAdapter { + + static create() { + return express(); + } + + static createRouter(): express.Router { + return express.Router(); + } +} \ No newline at end of file diff --git a/src/core/exceptions/exception.ts b/src/core/exceptions/exception.ts new file mode 100644 index 000000000..a7305d14b --- /dev/null +++ b/src/core/exceptions/exception.ts @@ -0,0 +1,14 @@ +export class Exception { + + constructor( + private readonly message: string, + private readonly status: number) {} + + getMessage() { + return this.message; + } + + getStatus() { + return this.status; + } +} diff --git a/src/core/exceptions/exceptions-handler.ts b/src/core/exceptions/exceptions-handler.ts new file mode 100644 index 000000000..f266b1038 --- /dev/null +++ b/src/core/exceptions/exceptions-handler.ts @@ -0,0 +1,17 @@ +import { Exception } from "./exception"; + +export class ExceptionsHandler { + private UNKOWN_EXCEPTION_MSG = "Unkown exception"; + + next(exception: Error | Exception, response) { + if (!(exception instanceof Exception)) { + response.status(500).json({ message: this.UNKOWN_EXCEPTION_MSG }); + return; + } + + response.status(exception.getStatus()).json({ + message: exception.getMessage() + }); + } + +} diff --git a/src/core/index.ts b/src/core/index.ts new file mode 100644 index 000000000..607db7ce4 --- /dev/null +++ b/src/core/index.ts @@ -0,0 +1,3 @@ +export { Exception } from './exceptions/exception'; +export { Middleware, MiddlewareConfiguration } from './middlewares/interfaces'; +export { MiddlewareBuilder } from './middlewares/builder'; \ No newline at end of file diff --git a/src/core/injector/container.ts b/src/core/injector/container.ts new file mode 100644 index 000000000..5a69a9d71 --- /dev/null +++ b/src/core/injector/container.ts @@ -0,0 +1,76 @@ +import { Controller, Injectable, NestModule } from "../../common/interfaces/"; +import { UnkownExportException } from "../../errors/exceptions/unkown-export.exception"; + +export class NestContainer { + private readonly modules = new Map(); + + addModule(moduleClass) { + if(!this.modules.has(moduleClass)) { + this.modules.set(moduleClass, { + instance: new moduleClass(), + relatedModules: new Set(), + components: new Map>(), + routes: new Map>(), + exports: new Set(), + }); + } + } + + getModules(): Map { + return this.modules; + } + + addRelatedModule(relatedModule: NestModule, module: NestModule) { + if(this.modules.has(module)) { + const storedModule = this.modules.get(module); + const related = this.modules.get(relatedModule); + + storedModule.relatedModules.add(related); + } + } + + addComponent(component: Injectable, module: NestModule) { + if(this.modules.has(module)) { + const storedModule = this.modules.get(module); + storedModule.components.set(component, { + instance: null, + isResolved: false, + }); + } + } + + addExportedComponent(exportedComponent: Injectable, module: NestModule) { + if(this.modules.has(module)) { + const storedModule = this.modules.get(module); + if (!storedModule.components.get(exportedComponent)) { + throw new UnkownExportException(); + } + storedModule.exports.add(exportedComponent); + } + + } + + addRoute(route: Controller, module: NestModule) { + if(this.modules.has(module)) { + const storedModule = this.modules.get(module); + storedModule.routes.set(route, { + instance: null, + isResolved: false, + }); + } + } + +} + +export interface ModuleDependencies { + instance: NestModule; + relatedModules?: Set; + components?: Map>; + routes?: Map>; + exports?: Set; +} + +export interface InstanceWrapper { + instance: T; + isResolved: boolean; +} \ No newline at end of file diff --git a/src/core/injector/injector.ts b/src/core/injector/injector.ts new file mode 100644 index 000000000..76548c905 --- /dev/null +++ b/src/core/injector/injector.ts @@ -0,0 +1,121 @@ +import "reflect-metadata"; +import { ModuleDependencies, InstanceWrapper } from "./container"; +import { Middleware } from "../middlewares/interfaces/middleware.interface"; +import { CircularDependencyException } from "../../errors/exceptions/circular-dependency.exception"; +import { UnkownDependenciesException } from "../../errors/exceptions/unkown-dependencies.exception"; +import { MiddlewareProto } from "../middlewares/interfaces/middleware-proto.interface"; +import { RuntimeException } from "../../errors/exceptions/runtime.exception"; + +export class Injector { + + loadInstanceOfMiddleware( + middlewareType, + collection: Map, + module: ModuleDependencies) { + + const currentFetchedMiddleware = collection.get(middlewareType); + + if(currentFetchedMiddleware === null) { + this.resolveConstructorParams(middlewareType, module, (argsInstances) => { + collection.set(middlewareType, new middlewareType(...argsInstances)) + }); + } + } + + loadInstanceOfRoute(routeType, module: ModuleDependencies) { + const routes = module.routes; + this.loadInstance(routeType, routes, module); + } + + loadPrototypeOfInstance(type, collection: Map>) { + if (!collection) { return; } + + collection.set(type, { + ...collection.get(type), + instance: Object.create(type.prototype), + }); + } + + loadInstanceOfComponent(componentType, module: ModuleDependencies) { + const components = module.components; + this.loadInstance(componentType, components, module); + } + + loadInstance(type, collection, module: ModuleDependencies) { + const currentFetchedInstance = collection.get(type); + if (typeof currentFetchedInstance === 'undefined') { + throw new RuntimeException(""); + } + if (!currentFetchedInstance.isResolved) { + this.resolveConstructorParams(type, module, (argsInstances) => { + currentFetchedInstance.instance = Object.assign( + currentFetchedInstance.instance, + new type(...argsInstances), + ); + currentFetchedInstance.isResolved = true; + }); + } + } + + private resolveConstructorParams(type, module, callback) { + let constructorParams = Reflect.getMetadata('design:paramtypes', type) || []; + + if ((type).dependencies) { + constructorParams = (type).dependencies; + } + const argsInstances = constructorParams.map((param) => ( + this.resolveSingleParam(type, param, module) + )); + callback(argsInstances); + } + + private resolveSingleParam(targetType, param, module: ModuleDependencies) { + if (typeof param === "undefined") { + throw new CircularDependencyException(targetType); + } + + return this.resolveComponentInstance(module, param, targetType); + } + + private resolveComponentInstance(module: ModuleDependencies, param, componentType) { + const components = module.components; + const instanceWrapper = this.scanForComponent(components, param, module, componentType); + + if (instanceWrapper.instance === null) { + this.loadInstanceOfComponent(param, module); + } + return instanceWrapper.instance; + } + + private scanForComponent(components, param, module, componentType) { + if (!components.has(param)) { + const instanceWrapper = this.scanForComponentInRelatedModules(module, param); + + if (instanceWrapper === null) { + throw new UnkownDependenciesException(componentType); + } + return instanceWrapper; + } + return components.get(param); + } + + private scanForComponentInRelatedModules(module: ModuleDependencies, componentType) { + const relatedModules = module.relatedModules; + let component = null; + + relatedModules.forEach((relatedModule) => { + const { components, exports } = relatedModule; + + if (!exports.has(componentType) || !components.has(componentType)) { + return; + } + + component = components.get(componentType); + if (!component.isResolved) { + this.loadInstanceOfComponent(componentType, relatedModule); + } + }); + return component; + } + +} \ No newline at end of file diff --git a/src/core/injector/instance-loader.ts b/src/core/injector/instance-loader.ts new file mode 100644 index 000000000..ae6cec6af --- /dev/null +++ b/src/core/injector/instance-loader.ts @@ -0,0 +1,56 @@ +import { NestContainer, ModuleDependencies } from "./container"; +import { Injector } from "./injector"; +import { Injectable } from "../../common/interfaces/injectable.interface"; +import { Controller } from "../../common/interfaces/controller.interface"; + +export class InstanceLoader { + private injector = new Injector(); + + constructor(private container: NestContainer) {} + + createInstancesOfDependencies() { + const modules = this.container.getModules(); + + this.createPrototypes(modules); + this.createInstances(modules); + } + + private createPrototypes(modules) { + modules.forEach((module) => { + this.createPrototypesOfComponents(module); + this.createPrototypesOfRoutes(module); + }); + } + + private createInstances(modules) { + modules.forEach((module) => { + this.createInstancesOfComponents(module); + this.createInstancesOfRoutes(module); + }) + } + + private createPrototypesOfComponents(module: ModuleDependencies) { + module.components.forEach((wrapper, componentType) => { + this.injector.loadPrototypeOfInstance(componentType, module.components); + }); + } + + private createInstancesOfComponents(module: ModuleDependencies) { + module.components.forEach((wrapper, componentType) => { + this.injector.loadInstanceOfComponent(componentType, module); + }); + } + + private createPrototypesOfRoutes(module: ModuleDependencies) { + module.routes.forEach((wrapper, routeType) => { + this.injector.loadPrototypeOfInstance(routeType, module.routes); + }); + } + + private createInstancesOfRoutes(module: ModuleDependencies) { + module.routes.forEach((wrapper, routeType) => { + this.injector.loadInstanceOfRoute(routeType, module); + }); + } + +} \ No newline at end of file diff --git a/src/core/middlewares/builder.ts b/src/core/middlewares/builder.ts new file mode 100644 index 000000000..1d8d6c9c3 --- /dev/null +++ b/src/core/middlewares/builder.ts @@ -0,0 +1,22 @@ +import { MiddlewareConfiguration } from "./interfaces/middleware-configuration.interface"; +import { InvalidMiddlewareConfigurationException } from "../../errors/exceptions/invalid-middleware-configuration.exception"; + +export class MiddlewareBuilder { + private storedConfiguration = new Set(); + + use(configuration: MiddlewareConfiguration) { + if (typeof configuration.middlewares === "undefined" || + typeof configuration.forRoutes === "undefined") { + + throw new InvalidMiddlewareConfigurationException(); + } + + this.storedConfiguration.add(configuration); + return this; + } + + build() { + return [ ...this.storedConfiguration ]; + } + +} \ No newline at end of file diff --git a/src/core/middlewares/container.ts b/src/core/middlewares/container.ts new file mode 100644 index 000000000..739b50919 --- /dev/null +++ b/src/core/middlewares/container.ts @@ -0,0 +1,60 @@ +import { MiddlewareConfiguration } from "./interfaces/middleware-configuration.interface"; +import { Middleware } from "./interfaces/middleware.interface"; +import { MiddlewareProto } from "./interfaces/middleware-proto.interface"; +import { RoutesMapper } from "./routes-mapper"; +import { NestModule } from "../../common/interfaces/nest-module.interface"; +import { UnkownModuleException } from "../../errors/exceptions/unkown-module.exception"; + +export class MiddlewaresContainer { + private readonly middlewares = new Map>(); + private readonly configs = new Map>(); + + constructor(private routesMapper: RoutesMapper) {} + + getMiddlewares(module: NestModule): Map { + if (!this.middlewares.has(module)) { + throw new UnkownModuleException(); + } + return this.middlewares.get(module); + } + + getConfigs(): Map> { + return this.configs; + } + + addConfig(configList: MiddlewareConfiguration[], module: NestModule) { + const currentMiddlewares = this.getCurrentMiddlewares(module); + const currentConfig = this.getCurrentConfig(module); + + (configList || []).map((config) => { + [].concat(config.middlewares).map( + (middleware) => { + currentMiddlewares.set(middleware, null); + } + ); + + config.forRoutes = this.mapRoutesToFlatList(config.forRoutes); + currentConfig.add(config); + }); + } + + private mapRoutesToFlatList(forRoutes) { + return forRoutes.map((route) => ( + this.routesMapper.mapRouteToRouteProps(route) + )).reduce((a, b) => a.concat(b)); + } + + private getCurrentMiddlewares(module: NestModule) { + if (!this.middlewares.has(module)) { + this.middlewares.set(module, new Map()); + } + return this.middlewares.get(module); + } + + private getCurrentConfig(module: NestModule) { + if (!this.configs.has(module)) { + this.configs.set(module, new Set()); + } + return this.configs.get(module); + } +} diff --git a/src/core/middlewares/interfaces/index.ts b/src/core/middlewares/interfaces/index.ts new file mode 100644 index 000000000..efd41fb5d --- /dev/null +++ b/src/core/middlewares/interfaces/index.ts @@ -0,0 +1,2 @@ +export * from "./middleware-configuration.interface"; +export * from "./middleware.interface"; \ No newline at end of file diff --git a/src/core/middlewares/interfaces/middleware-configuration.interface.ts b/src/core/middlewares/interfaces/middleware-configuration.interface.ts new file mode 100644 index 000000000..6fc84004f --- /dev/null +++ b/src/core/middlewares/interfaces/middleware-configuration.interface.ts @@ -0,0 +1,9 @@ +import { ControllerMetadata } from "../../../common/interfaces/controller-metadata.interface"; +import { Controller } from "../../../common/interfaces/controller.interface"; +import { MiddlewareProto } from "./middleware-proto.interface"; +import { RequestMethod } from "../../../common/enums/request-method.enum"; + +export interface MiddlewareConfiguration { + middlewares: MiddlewareProto | MiddlewareProto[]; + forRoutes: (Controller | ControllerMetadata & { method?: RequestMethod })[]; +} \ No newline at end of file diff --git a/src/core/middlewares/interfaces/middleware-proto.interface.ts b/src/core/middlewares/interfaces/middleware-proto.interface.ts new file mode 100644 index 000000000..a23966fc5 --- /dev/null +++ b/src/core/middlewares/interfaces/middleware-proto.interface.ts @@ -0,0 +1,5 @@ +import { Middleware } from "./middleware.interface"; + +export interface MiddlewareProto { + new(): Middleware; +} \ No newline at end of file diff --git a/src/core/middlewares/interfaces/middleware.interface.ts b/src/core/middlewares/interfaces/middleware.interface.ts new file mode 100644 index 000000000..e667468a9 --- /dev/null +++ b/src/core/middlewares/interfaces/middleware.interface.ts @@ -0,0 +1,5 @@ +import { Request, Response, NextFunction } from "express"; + +export interface Middleware { + resolve: () => (req?: Request, res?: Response, next?: NextFunction) => void; +} \ No newline at end of file diff --git a/src/core/middlewares/middlewares-module.ts b/src/core/middlewares/middlewares-module.ts new file mode 100644 index 000000000..193ccccac --- /dev/null +++ b/src/core/middlewares/middlewares-module.ts @@ -0,0 +1,99 @@ +import { Application } from "express"; +import { NestContainer, ModuleDependencies } from "../injector/container"; +import { MiddlewareBuilder } from "./builder"; +import { MiddlewaresContainer } from "./container"; +import { MiddlewaresResolver } from "./resolver"; +import { ControllerMetadata } from "../../common/interfaces/controller-metadata.interface"; +import { NestModule } from "../../common/interfaces/nest-module.interface"; +import { MiddlewareConfiguration } from "./interfaces/middleware-configuration.interface"; +import { UnkownMiddlewareException } from "../../errors/exceptions/unkown-middleware.exception"; +import { InvalidMiddlewareException } from "../../errors/exceptions/invalid-middleware.exception"; +import { RequestMethod } from "../../common/enums/request-method.enum"; +import { RoutesMapper } from "./routes-mapper"; + +export class MiddlewaresModule { + private static container = new MiddlewaresContainer(new RoutesMapper()); + private static resolver: MiddlewaresResolver; + + static getContainer(): MiddlewaresContainer { + return this.container; + } + + static setup(container: NestContainer) { + this.resolver = new MiddlewaresResolver(this.container); + + const modules = container.getModules(); + this.resolveMiddlewares(modules); + } + + static resolveMiddlewares(modules: Map) { + modules.forEach((module, moduleProto) => { + const instance = module.instance; + + this.loadConfiguration(instance, moduleProto); + this.resolver.resolveInstances(module, moduleProto); + }); + } + + static loadConfiguration(instance, module: NestModule) { + if (!instance.configure) { + return; + } + + const middlewaresBuilder = new MiddlewareBuilder(); + instance.configure(middlewaresBuilder); + + if (middlewaresBuilder instanceof MiddlewareBuilder) { + const config = middlewaresBuilder.build(); + this.container.addConfig(config, module); + } + } + + static setupMiddlewares(app: Application) { + const configs = this.container.getConfigs(); + + configs.forEach((moduleConfigs, module) => { + [ ...moduleConfigs ].map((config: MiddlewareConfiguration) => { + + config.forRoutes.map((route: ControllerMetadata & { method: RequestMethod }) => { + this.setupRouteMiddleware(route, config, module, app); + }); + }); + }); + } + + static setupRouteMiddleware( + route: ControllerMetadata & { method: RequestMethod }, + config: MiddlewareConfiguration, + module: NestModule, + app: Application) { + + const { path, method } = route; + + [].concat(config.middlewares).map((middlewareType) => { + const middlewaresCollection = this.container.getMiddlewares(module); + const middleware = middlewaresCollection.get(middlewareType); + + if (typeof middleware === "undefined") { + throw new UnkownMiddlewareException(); + } + if (typeof middleware.resolve === "undefined") { + throw new InvalidMiddlewareException(); + } + const router = this.findRouterMethod(app, method).bind(app); + router(path, middleware.resolve()); + }); + } + + private static findRouterMethod(app, requestMethod: RequestMethod) { + switch(requestMethod) { + case RequestMethod.POST: { return app.post; } + case RequestMethod.ALL: { return app.all; } + case RequestMethod.DELETE: { return app.delete; } + case RequestMethod.PUT: { return app.put; } + default: { + return app.get; + } + } + } +} \ No newline at end of file diff --git a/src/core/middlewares/resolver.ts b/src/core/middlewares/resolver.ts new file mode 100644 index 000000000..445409b12 --- /dev/null +++ b/src/core/middlewares/resolver.ts @@ -0,0 +1,23 @@ +import { ModuleDependencies } from "../injector/container"; +import { MiddlewaresContainer } from "./container"; +import { Injector } from "../injector/injector"; +import { NestModule } from "../../common/interfaces/nest-module.interface"; + +export class MiddlewaresResolver { + private instanceLoader = new Injector(); + + constructor(private middlewaresContainer: MiddlewaresContainer) {} + + resolveInstances(module: ModuleDependencies, moduleProto: NestModule) { + const middlewares = this.middlewaresContainer.getMiddlewares(moduleProto); + + middlewares.forEach((val, middlewareType) => { + this.instanceLoader.loadInstanceOfMiddleware( + middlewareType, + middlewares, + module + ); + }); + } + +} diff --git a/src/core/middlewares/routes-mapper.ts b/src/core/middlewares/routes-mapper.ts new file mode 100644 index 000000000..28a9291ec --- /dev/null +++ b/src/core/middlewares/routes-mapper.ts @@ -0,0 +1,42 @@ +import "reflect-metadata"; +import { RouterBuilder } from "../router/router-builder"; +import { UnkownRequestMappingException } from "../../errors/exceptions/unkown-request-mapping.exception"; +import { RequestMethod } from "../../common/enums/request-method.enum"; + +export class RoutesMapper { + private readonly routerBuilder = new RouterBuilder(); + + mapRouteToRouteProps(routeProto) { + const routePath: string = Reflect.getMetadata("path", routeProto); + + if (typeof routePath === "undefined") { + return [ this.mapObjectToRouteProps(routeProto) ]; + } + + const paths = this.routerBuilder.scanForPathsFromPrototype( + Object.create(routeProto), + routeProto.prototype + ); + + return paths.map((singlePath) => ({ + path: this.validateRoutePath(routePath) + this.validateRoutePath(singlePath.path), + method: singlePath.requestMethod + })); + } + + private mapObjectToRouteProps(route) { + if (typeof route.path === "undefined") { + throw new UnkownRequestMappingException(); + } + + return { + path: this.validateRoutePath(route.path), + method: (typeof route.method === "undefined") ? RequestMethod.ALL : route.method + }; + } + + private validateRoutePath(routePath: string): string { + return (routePath.charAt(0) !== '/') ? '/' + routePath : routePath; + } + +} diff --git a/src/core/router/router-builder.ts b/src/core/router/router-builder.ts new file mode 100644 index 000000000..22fa9be01 --- /dev/null +++ b/src/core/router/router-builder.ts @@ -0,0 +1,97 @@ +import "reflect-metadata"; +import { Controller } from "../../common/interfaces/controller.interface"; +import { RequestMethod } from "../../common/enums/request-method.enum"; +import { RouterProxy, RouterProxyCallback } from "./router-proxy"; +import { UnkownRequestMappingException } from "../../errors/exceptions/unkown-request-mapping.exception"; +import { ExpressAdapter } from "../adapters/express-adapter"; + +export class RouterBuilder { + + constructor( + private routerProxy?: RouterProxy, + private expressAdapter?: ExpressAdapter) {} + + public build(instance: Controller, routePrototype: Function) { + const router = (this.expressAdapter).createRouter(); + const path = this.fetchRouterPath(routePrototype); + const routerPaths = this.scanForPaths(instance); + + this.applyPathsToRouterProxy(router, routerPaths); + + return { path, router }; + } + + scanForPaths(instance: Controller): RoutePathProperties[] { + const instancePrototype = Object.getPrototypeOf(instance); + return this.scanForPathsFromPrototype(instance, instancePrototype); + } + + scanForPathsFromPrototype(instance: Controller, instancePrototype) { + return Object.getOwnPropertyNames(instancePrototype) + .filter((method) => method !== "constructor") + .map((methodName) => this.exploreMethodMetadata(instance, instancePrototype, methodName)) + .filter((path) => path !== null); + } + + exploreMethodMetadata(instance, instancePrototype, methodName: string): RoutePathProperties { + const callbackMethod = instancePrototype[methodName]; + + const routePath = Reflect.getMetadata("path", callbackMethod); + if(typeof routePath === "undefined") { + return null; + } + + const requestMethod: RequestMethod = Reflect.getMetadata("method", callbackMethod); + return { + targetCallback: (callbackMethod).bind(instance), + path: this.validateRoutePath(routePath), + requestMethod, + }; + } + + applyPathsToRouterProxy(router, routePaths: RoutePathProperties[]) { + (routePaths || []).map((pathProperties) => { + this.bindMethodToRouterProxy(router, pathProperties); + }); + } + + private bindMethodToRouterProxy(router, pathProperties: RoutePathProperties) { + const { path, requestMethod, targetCallback } = pathProperties; + + const routerMethod = this.findRouterMethod(router, requestMethod).bind(router); + const proxy = this.routerProxy.createProxy(targetCallback); + + routerMethod(path, proxy); + } + + private findRouterMethod(router, requestMethod: RequestMethod) { + switch(requestMethod) { + case RequestMethod.POST: { return router.post; } + case RequestMethod.ALL: { return router.all; } + case RequestMethod.DELETE: { return router.delete; } + case RequestMethod.PUT: { return router.put; } + default: { + return router.get; + } + } + } + + private fetchRouterPath(routePrototype: Function) { + const path = Reflect.getMetadata("path", routePrototype); + return this.validateRoutePath(path); + } + + private validateRoutePath(routePath: string): string { + if(typeof routePath === "undefined") { + throw new UnkownRequestMappingException(); + } + return (routePath.charAt(0) !== '/') ? '/' + routePath : routePath; + } + +} + +interface RoutePathProperties { + path: string, + requestMethod: RequestMethod, + targetCallback: RouterProxyCallback, +} \ No newline at end of file diff --git a/src/core/router/router-proxy.ts b/src/core/router/router-proxy.ts new file mode 100644 index 000000000..b54edd921 --- /dev/null +++ b/src/core/router/router-proxy.ts @@ -0,0 +1,24 @@ +import { ExceptionsHandler } from "../exceptions/exceptions-handler"; + +export class RouterProxy { + + constructor(private exceptionsHandler: ExceptionsHandler) {} + + createProxy(targetCallback: RouterProxyCallback) { + return (req, res, next) => { + try { + Promise.resolve(targetCallback(req, res, next)).catch((e) => { + this.exceptionsHandler.next(e, res); + }); + } + catch(e) { + this.exceptionsHandler.next(e, res); + } + } + } + +} + +export interface RouterProxyCallback { + (req?, res?, next?): void; +} diff --git a/src/core/router/routes-resolver.ts b/src/core/router/routes-resolver.ts new file mode 100644 index 000000000..45d9f2d68 --- /dev/null +++ b/src/core/router/routes-resolver.ts @@ -0,0 +1,31 @@ +import { Application } from "express"; +import { NestContainer, InstanceWrapper } from "../injector/container"; +import { RouterBuilder } from "./router-builder"; +import { RouterProxy } from "./router-proxy"; +import { ExceptionsHandler } from "../exceptions/exceptions-handler"; +import { Controller } from "../../common/interfaces/controller.interface"; + +export class RoutesResolver { + private readonly routerProxy = new RouterProxy(new ExceptionsHandler()); + private routerBuilder: RouterBuilder; + + constructor(private container: NestContainer, expressAdapter) { + this.routerBuilder = new RouterBuilder(this.routerProxy, expressAdapter); + } + + resolve(expressInstance: Application) { + const modules = this.container.getModules(); + modules.forEach(({ routes }) => this.setupRouters(routes, expressInstance)); + } + + setupRouters( + routes: Map>, + expressInstance: Application) { + + routes.forEach(({ instance }, routePrototype: Function) => { + const { path, router } = this.routerBuilder.build(instance, routePrototype); + + expressInstance.use(path, router); + }); + } +} \ No newline at end of file diff --git a/src/core/scanner.ts b/src/core/scanner.ts new file mode 100644 index 000000000..1211d7462 --- /dev/null +++ b/src/core/scanner.ts @@ -0,0 +1,75 @@ +import "reflect-metadata"; +import { NestContainer } from "./injector/container"; +import { NestModule } from "../common/interfaces/nest-module.interface"; +import { Controller } from "../common/interfaces/controller.interface"; +import { Injectable } from "../common/interfaces/injectable.interface"; + +export class DependenciesScanner { + + constructor(private container: NestContainer) {} + + scan(module: NestModule) { + this.scanForModules(module); + this.scanModulesForDependencies(); + } + + private scanForModules(module: NestModule) { + this.storeModule(module); + + const innerModules = Reflect.getMetadata('modules', module) || []; + innerModules.map((module) => this.scanForModules(module)); + } + + private storeModule(module: NestModule) { + this.container.addModule(module); + } + + private scanModulesForDependencies() { + const modules = this.container.getModules(); + + modules.forEach((deps, module) => { + this.reflectRelatedModules(module); + this.reflectComponents(module); + this.reflectRoutes(module); + this.reflectExports(module); + }); + } + + + private reflectRelatedModules(module: NestModule) { + const modules = Reflect.getMetadata('modules', module) || []; + modules.map((related) => this.storeRelatedModule(related, module)); + } + + private reflectComponents(module: NestModule) { + const components = Reflect.getMetadata('components', module) || []; + components.map((component) => this.storeComponent(component, module)); + } + + private reflectRoutes(module: NestModule) { + const routes = Reflect.getMetadata('controllers', module) || []; + routes.map((route) => this.storeRoute(route, module)); + } + + private reflectExports(module: NestModule) { + const exports = Reflect.getMetadata('exports', module) || []; + exports.map((exportedComponent) => this.storeExportedComponent(exportedComponent, module)); + } + + private storeRelatedModule(related: NestModule, module: NestModule) { + this.container.addRelatedModule(related, module); + } + + private storeComponent(component: Injectable, module: NestModule) { + this.container.addComponent(component, module); + } + + private storeExportedComponent(exportedComponent: Injectable, module: NestModule) { + this.container.addExportedComponent(exportedComponent, module); + } + + private storeRoute(route: Controller, module: NestModule) { + this.container.addRoute(route, module); + } + +} \ No newline at end of file diff --git a/src/core/test/exceptions/exceptions-handler.spec.ts b/src/core/test/exceptions/exceptions-handler.spec.ts new file mode 100644 index 000000000..60e247aa7 --- /dev/null +++ b/src/core/test/exceptions/exceptions-handler.spec.ts @@ -0,0 +1,46 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { ExceptionsHandler } from "../../exceptions/exceptions-handler"; +import { Exception } from "../../exceptions/exception"; + +describe('ExceptionsHandler', () => { + let handler: ExceptionsHandler; + let statusStub: sinon.SinonStub; + let jsonStub: sinon.SinonStub; + let response; + + beforeEach(() => { + handler = new ExceptionsHandler(); + statusStub = sinon.stub(); + jsonStub = sinon.stub(); + + response = { + status: statusStub, + json: jsonStub + }; + response.status.returns(response); + response.json.returns(response); + }); + + describe('next', () => { + + it('should method send expected response status code and message when exception is unknown', () => { + handler.next(new Error(), response); + + expect(statusStub.calledWith(500)).to.be.true; + expect(jsonStub.calledWith({ message: "Unkown exception" })).to.be.true; + }); + + it('should method send expected response status code and message when exception is instance of Exception', () => { + const status = 401; + const message = "Unauthorized"; + + handler.next(new Exception(message, status), response); + + expect(statusStub.calledWith(status)).to.be.true; + expect(jsonStub.calledWith({ message })).to.be.true; + }); + + }); + +}); \ No newline at end of file diff --git a/src/core/test/injector/container.spec.ts b/src/core/test/injector/container.spec.ts new file mode 100644 index 000000000..51808e81d --- /dev/null +++ b/src/core/test/injector/container.spec.ts @@ -0,0 +1,39 @@ +import { expect } from "chai"; +import { NestContainer, ModuleDependencies, InstanceWrapper } from "../../injector/container"; +import { Module } from "../../../common/utils/module.decorator"; +import { Injectable } from "../../../common/interfaces/injectable.interface"; +import { Controller } from "../../../common/interfaces/controller.interface"; +import { UnkownExportException } from "../../../errors/exceptions/unkown-export.exception"; + +describe('NestContainer', () => { + let container: NestContainer; + + @Module({}) + class TestModule {} + + beforeEach(() => { + container = new NestContainer(); + }); + + it('should create module instance and collections for dependencies', () => { + container.addModule(TestModule); + + expect(container["modules"].get(TestModule)).to.be.deep.equal({ + instance: new TestModule(), + relatedModules: new Set(), + components: new Map>(), + routes: new Map>(), + exports: new Set(), + }) + }); + + + it('should throw "UnkownExportException" when given exported component is not a part of components array', () => { + container.addModule(TestModule); + + expect( + container.addExportedComponent.bind(container, "Test", TestModule) + ).throws(UnkownExportException); + }); + +}); \ No newline at end of file diff --git a/src/core/test/injector/injector.spec.ts b/src/core/test/injector/injector.spec.ts new file mode 100644 index 000000000..8c0f6f161 --- /dev/null +++ b/src/core/test/injector/injector.spec.ts @@ -0,0 +1,101 @@ +import { expect } from "chai"; +import { ModuleDependencies, InstanceWrapper } from "../../injector/container"; +import { Injector } from "../../injector/injector"; +import { Component } from "../../../common/utils/component.decorator"; +import { RuntimeException } from "../../../errors/exceptions/runtime.exception"; + +describe('Injector', () => { + let injector: Injector; + + beforeEach(() => { + injector = new Injector(); + }); + + describe('loadInstance', () => { + + @Component() + class DependencyOne {} + + @Component() + class DependencyTwo {} + + @Component() + class MainTest { + constructor( + public depOne: DependencyOne, + public depTwo: DependencyTwo) {} + } + + let moduleDeps: ModuleDependencies; + + beforeEach(() => { + moduleDeps = { + instance: null, + components: new Map>(), + }; + moduleDeps.components.set(MainTest, { + instance: Object.create(MainTest.prototype), + isResolved: false + }); + moduleDeps.components.set(DependencyOne, { + instance: Object.create(DependencyOne.prototype), + isResolved: false + }); + moduleDeps.components.set(DependencyTwo, { + instance: Object.create(DependencyOne.prototype), + isResolved: false + }); + }); + + it('should create an instance of component with proper dependencies', () => { + injector.loadInstance(MainTest, moduleDeps.components, moduleDeps); + const { instance } = >(moduleDeps.components.get(MainTest)); + + expect(instance.depOne instanceof DependencyOne).to.be.true; + expect(instance.depTwo instanceof DependencyOne).to.be.true; + expect(instance instanceof MainTest).to.be.true; + }); + + it('should set "isResolved" property to true after instance initialization', () => { + injector.loadInstance(MainTest, moduleDeps.components, moduleDeps); + const { isResolved } = >(moduleDeps.components.get(MainTest)); + expect(isResolved).to.be.true; + }); + + it('should throw RuntimeException when type is not stored in collection', () => { + expect( + injector.loadInstance.bind(injector, "Test", moduleDeps.components, moduleDeps) + ).to.throw(RuntimeException); + }); + + }); + + describe('loadPrototypeOfInstance', () => { + + @Component() + class Test {} + + let moduleDeps: ModuleDependencies; + + beforeEach(() => { + moduleDeps = { + instance: null, + components: new Map>(), + }; + moduleDeps.components.set(Test, { + instance: Object.create(Test.prototype), + isResolved: false + }); + }); + + it('should create prototype of instance', () => { + const expectedResult = { + instance: Object.create(Test.prototype), + isResolved: false + }; + injector.loadPrototypeOfInstance(Test, moduleDeps.components); + expect(moduleDeps.components.get(Test)).to.deep.equal(expectedResult); + }); + }); + +}); \ No newline at end of file diff --git a/src/core/test/injector/instance-loader.spec.ts b/src/core/test/injector/instance-loader.spec.ts new file mode 100644 index 000000000..52ffefe3f --- /dev/null +++ b/src/core/test/injector/instance-loader.spec.ts @@ -0,0 +1,93 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { InstanceLoader } from "../../injector/instance-loader"; +import { NestContainer } from "../../injector/container"; +import { Injector } from "../../injector/injector"; +import { Controller } from "../../../common/utils/controller.decorator"; +import { Component } from "../../../common/utils/component.decorator"; + +describe('NestContainer', () => { + let loader: InstanceLoader; + let container: NestContainer; + let mockContainer: sinon.SinonMock; + + @Controller({ path: "" }) + class TestRoute {} + + @Component() + class TestComponent {} + + beforeEach(() => { + container = new NestContainer(); + loader = new InstanceLoader(container); + mockContainer = sinon.mock(container); + }); + + it('should call "loadPrototypeOfInstance" for each component and route in each module', () => { + const injector = new Injector(); + loader["injector"] = injector; + + const module = { + components: new Map(), + routes: new Map(), + }; + module.components.set(TestComponent, { instance: null }); + module.routes.set(TestRoute, { instance: null }); + + const modules = new Map(); + modules.set("Test", module); + mockContainer.expects("getModules").returns(modules); + + const loadComponentPrototypeStub = sinon.stub(injector, "loadPrototypeOfInstance"); + + sinon.stub(injector, "loadInstanceOfRoute"); + sinon.stub(injector, "loadInstanceOfComponent"); + + loader.createInstancesOfDependencies(); + expect(loadComponentPrototypeStub.calledWith(TestComponent, module.components)).to.be.true; + expect(loadComponentPrototypeStub.calledWith(TestRoute, module.components)).to.be.true; + }); + + it('should call "loadInstanceOfComponent" for each component in each module', () => { + const injector = new Injector(); + loader["injector"] = injector; + + const module = { + components: new Map(), + routes: new Map(), + }; + module.components.set(TestComponent, { instance: null }); + + const modules = new Map(); + modules.set("Test", module); + mockContainer.expects("getModules").returns(modules); + + const loadComponentStub = sinon.stub(injector, "loadInstanceOfComponent"); + sinon.stub(injector, "loadInstanceOfRoute"); + + loader.createInstancesOfDependencies(); + expect(loadComponentStub.calledWith(TestComponent, module)).to.be.true; + }); + + it('should call "loadInstanceOfRoute" for each route in each module', () => { + const injector = new Injector(); + loader["injector"] = injector; + + const module = { + components: new Map(), + routes: new Map(), + }; + module.routes.set(TestRoute, { instance: null }); + + const modules = new Map(); + modules.set("Test", module); + mockContainer.expects("getModules").returns(modules); + + sinon.stub(injector, "loadInstanceOfComponent"); + const loadRoutesStub = sinon.stub(injector, "loadInstanceOfRoute"); + + loader.createInstancesOfDependencies(); + expect(loadRoutesStub.calledWith(TestRoute, module)).to.be.true; + }); + +}); \ No newline at end of file diff --git a/src/core/test/middlewares/builder.spec.ts b/src/core/test/middlewares/builder.spec.ts new file mode 100644 index 000000000..787e9ff67 --- /dev/null +++ b/src/core/test/middlewares/builder.spec.ts @@ -0,0 +1,45 @@ +import { expect } from "chai"; +import { MiddlewareBuilder } from "../../middlewares/builder"; +import { InvalidMiddlewareConfigurationException } from "../../../errors/exceptions/invalid-middleware-configuration.exception"; + +describe('MiddlewareBuilder', () => { + let builder: MiddlewareBuilder; + + beforeEach(() => { + builder = new MiddlewareBuilder(); + }); + + it('should store configuration passed as argument', () => { + builder.use({ + middlewares: "Test", + forRoutes: "Test" + }); + + expect(builder.build()).to.deep.equal([{ + middlewares: "Test", + forRoutes: "Test" + }]); + }); + + it('should be possible to chain "use" calls', () => { + builder.use({ + middlewares: "Test", + forRoutes: "Test" + }).use({ + middlewares: "Test", + forRoutes: "Test" + }); + expect(builder.build()).to.deep.equal([{ + middlewares: "Test", + forRoutes: "Test" + }, { + middlewares: "Test", + forRoutes: "Test" + }]); + }); + + it('should throw exception when middleware configuration object is invalid', () => { + expect(builder.use.bind(builder, "test")).throws(InvalidMiddlewareConfigurationException); + }); + +}); \ No newline at end of file diff --git a/src/core/test/middlewares/container.spec.ts b/src/core/test/middlewares/container.spec.ts new file mode 100644 index 000000000..ef8743ec5 --- /dev/null +++ b/src/core/test/middlewares/container.spec.ts @@ -0,0 +1,61 @@ +import { expect } from "chai"; +import { MiddlewaresContainer } from "../../middlewares/container"; +import { MiddlewareConfiguration } from "../../middlewares/interfaces/middleware-configuration.interface"; +import { Middleware } from "../../middlewares/interfaces/middleware.interface"; +import { Component } from "../../../common/utils/component.decorator"; +import { RoutesMapper } from "../../middlewares/routes-mapper"; +import { Controller } from "../../../common/utils/controller.decorator"; +import { RequestMapping } from "../../../common/utils/request-mapping.decorator"; +import { RequestMethod } from "../../../common/enums/request-method.enum"; + +describe('MiddlewaresContainer', () => { + @Controller({ path: "test" }) + class TestRoute { + + @RequestMapping({ path: "test" }) + getTest() {} + + @RequestMapping({ path: "another", method: RequestMethod.DELETE }) + getAnother() {} + } + + @Component() + class TestMiddleware implements Middleware { + resolve() { + return (req, res, next) => {} + } + } + + let container: MiddlewaresContainer; + + beforeEach(() => { + container = new MiddlewaresContainer(new RoutesMapper()); + }); + + it('should store expected configurations for given module', () => { + const config: MiddlewareConfiguration[] = [{ + middlewares: [ TestMiddleware ], + forRoutes: [ + TestRoute, + { path: "test" } + ] + } + ]; + container.addConfig(config, "Module"); + expect([ ...container.getConfigs().get("Module") ]).to.deep.equal(config); + }); + + it('should store expected middlewares for given module', () => { + const config: MiddlewareConfiguration[] = [{ + middlewares: TestMiddleware, + forRoutes: [ TestRoute ] + } + ]; + + const key = "Test"; + container.addConfig(config, key); + expect(container.getMiddlewares(key).size).to.eql(config.length); + expect(container.getMiddlewares(key).get(TestMiddleware)).to.eql(null); + }); + +}); \ No newline at end of file diff --git a/src/core/test/middlewares/middlewares-module.spec.ts b/src/core/test/middlewares/middlewares-module.spec.ts new file mode 100644 index 000000000..29f50e49d --- /dev/null +++ b/src/core/test/middlewares/middlewares-module.spec.ts @@ -0,0 +1,116 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Middleware } from "../../middlewares/interfaces/middleware.interface"; +import { Component } from "../../../common/utils/component.decorator"; +import { MiddlewareBuilder } from "../../middlewares/builder"; +import { MiddlewaresModule } from "../../middlewares/middlewares-module"; +import { UnkownMiddlewareException } from "../../../errors/exceptions/unkown-middleware.exception"; +import { InvalidMiddlewareException } from "../../../errors/exceptions/invalid-middleware.exception"; +import { RequestMethod } from "../../../common/enums/request-method.enum"; +import { Controller } from "../../../common/utils/controller.decorator"; +import { RequestMapping } from "../../../common/utils/request-mapping.decorator"; + +describe('MiddlewaresModule', () => { + @Controller({ path: "test" }) + class AnotherRoute { } + + @Controller({ path: "test" }) + class TestRoute { + + @RequestMapping({ path: "test" }) + getTest() {} + + @RequestMapping({ path: "another", method: RequestMethod.DELETE }) + getAnother() {} + } + + @Component() + class TestMiddleware implements Middleware { + resolve() { + return (req, res, next) => {} + } + } + + describe('loadConfiguration', () => { + + it('should call "configure" method if method is implemented', () => { + const configureSpy = sinon.spy(); + const mockModule = { + configure: configureSpy + }; + + MiddlewaresModule.loadConfiguration(mockModule, "Test"); + + expect(configureSpy.calledOnce).to.be.true; + expect(configureSpy.calledWith(new MiddlewareBuilder())).to.be.true; + }); + }); + + describe('setupRouteMiddleware', () => { + + it('should throw "UnkownMiddlewareException" exception when middlewares is not stored in container', () => { + const route = { path: "Test" }; + const configuration = { + middlewares: [ TestMiddleware ], + forRoutes: [ TestRoute ] + }; + + const useSpy = sinon.spy(); + const app = { use: useSpy }; + + expect(MiddlewaresModule.setupRouteMiddleware.bind( + MiddlewaresModule, route, configuration, "Test", app + )).throws(UnkownMiddlewareException); + }); + + it('should throw "InvalidMiddlewareException" exception when middlewares does not have "resolve" method', () => { + @Component() + class InvalidMiddleware {} + + const route = { path: "Test" }; + const configuration = { + middlewares: [ InvalidMiddleware ], + forRoutes: [ TestRoute ] + }; + + const useSpy = sinon.spy(); + const app = { use: useSpy }; + + const container = MiddlewaresModule.getContainer(); + const moduleKey = "Test"; + container.addConfig([ configuration ], moduleKey); + + const instance = new InvalidMiddleware(); + container.getMiddlewares(moduleKey).set(InvalidMiddleware, instance); + + expect(MiddlewaresModule.setupRouteMiddleware.bind( + MiddlewaresModule, route, configuration, moduleKey, app + )).throws(InvalidMiddlewareException); + }); + + it('should store middlewares when middleware is stored in container', () => { + const route = { path: "Test", method: RequestMethod.GET }; + const configuration = { + middlewares: [ TestMiddleware ], + forRoutes: [ { path: "test" }, AnotherRoute, TestRoute ] + }; + + const useSpy = sinon.spy(); + const app = { + get: useSpy + }; + + const container = MiddlewaresModule.getContainer(); + const moduleKey = "Test"; + container.addConfig([ configuration ], moduleKey); + + const instance = new TestMiddleware(); + container.getMiddlewares(moduleKey).set(TestMiddleware, instance); + + MiddlewaresModule.setupRouteMiddleware(route, configuration, moduleKey, app); + expect(useSpy.calledOnce).to.be.true; + }); + + }); + +}); \ No newline at end of file diff --git a/src/core/test/middlewares/resolver.spec.ts b/src/core/test/middlewares/resolver.spec.ts new file mode 100644 index 000000000..b3fba110b --- /dev/null +++ b/src/core/test/middlewares/resolver.spec.ts @@ -0,0 +1,44 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { MiddlewaresResolver } from "../../middlewares/resolver"; +import { MiddlewaresContainer } from "../../middlewares/container"; +import { Component } from "../../../common/utils/component.decorator"; +import { Middleware } from "../../middlewares/interfaces/middleware.interface"; +import { RoutesMapper } from "../../middlewares/routes-mapper"; + +describe('MiddlewaresResolver', () => { + @Component() + class TestMiddleware implements Middleware { + resolve() { + return (req, res, next) => {} + } + } + + let resolver: MiddlewaresResolver; + let container: MiddlewaresContainer; + let mockContainer: sinon.SinonMock; + + beforeEach(() => { + container = new MiddlewaresContainer(new RoutesMapper()); + resolver = new MiddlewaresResolver(container); + mockContainer = sinon.mock(container); + }); + + it('should resolve middleware instances from container', () => { + const loadInstanceOfMiddleware = sinon.stub(resolver["instanceLoader"], "loadInstanceOfMiddleware"); + const middlewares = new Map(); + middlewares.set(TestMiddleware, null); + + mockContainer.expects("getMiddlewares").returns(middlewares); + resolver.resolveInstances(null, null); + + expect(loadInstanceOfMiddleware.callCount).to.be.equal(middlewares.size); + expect(loadInstanceOfMiddleware.calledWith( + TestMiddleware, + middlewares, + null + )).to.be.true; + + loadInstanceOfMiddleware.restore(); + }); +}); \ No newline at end of file diff --git a/src/core/test/middlewares/routes-mapper.spec.ts b/src/core/test/middlewares/routes-mapper.spec.ts new file mode 100644 index 000000000..25d67c2ff --- /dev/null +++ b/src/core/test/middlewares/routes-mapper.spec.ts @@ -0,0 +1,58 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { RoutesMapper } from "../../middlewares/routes-mapper"; +import { Controller } from "../../../common/utils/controller.decorator"; +import { RequestMapping } from "../../../common/utils/request-mapping.decorator"; +import { RequestMethod } from "../../../common/enums/request-method.enum"; +import { UnkownRequestMappingException } from "../../../errors/exceptions/unkown-request-mapping.exception"; + +describe('RoutesMapper', () => { + @Controller({ path: "test" }) + class TestRoute { + + @RequestMapping({ path: "test" }) + getTest() {} + + @RequestMapping({ path: "another", method: RequestMethod.DELETE }) + getAnother() {} + } + + let mapper: RoutesMapper; + + beforeEach(() => { + mapper = new RoutesMapper(); + }); + + it('should map @Controller() to "ControllerMetadata" in forRoutes', () => { + const config = { + middlewares: "Test", + forRoutes: [ + { path: "test", method: RequestMethod.GET }, + TestRoute + ] + }; + + expect(mapper.mapRouteToRouteProps(config.forRoutes[0])).to.deep.equal([{ + path: "/test", method: RequestMethod.GET + }]); + + expect(mapper.mapRouteToRouteProps(config.forRoutes[1])).to.deep.equal([ + { path: "/test/test", method: RequestMethod.GET }, + { path: "/test/another", method: RequestMethod.DELETE }, + ]); + }); + + it('should throw exception when invalid object was passed as route', () => { + const config = { + middlewares: "Test", + forRoutes: [ + { method: RequestMethod.GET } + ] + }; + + expect( + mapper.mapRouteToRouteProps.bind(mapper, config.forRoutes[0]) + ).throws(UnkownRequestMappingException); + }); + +}); \ No newline at end of file diff --git a/src/core/test/router/router-builder.spec.ts b/src/core/test/router/router-builder.spec.ts new file mode 100644 index 000000000..c08c3b212 --- /dev/null +++ b/src/core/test/router/router-builder.spec.ts @@ -0,0 +1,74 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { RouterBuilder } from "../../router/router-builder"; +import { Controller } from "../../../common/utils/controller.decorator"; +import { RequestMapping } from "../../../common/utils/request-mapping.decorator"; +import { RequestMethod } from "../../../common/enums/request-method.enum"; + +describe('RouterBuilder', () => { + @Controller({ path: "global" }) + class TestRoute { + @RequestMapping({ path: "test" }) + getTest() {} + + @RequestMapping({ path: "test", method: RequestMethod.POST }) + postTest() {} + + @RequestMapping({ path: "another-test", method: RequestMethod.ALL }) + anotherTest() {} + + private simplePlainMethod() {} + } + + let routerBuilder: RouterBuilder; + beforeEach(() => { + routerBuilder = new RouterBuilder(null, null); + }); + + describe('scanForPathsFromPrototype', () => { + + it('should method return expected list of route paths', () => { + const paths = routerBuilder.scanForPathsFromPrototype(new TestRoute(), TestRoute.prototype); + + expect(paths).to.have.length(3); + + expect(paths[0].path).to.eql("/test"); + expect(paths[1].path).to.eql("/test"); + expect(paths[2].path).to.eql("/another-test"); + + expect(paths[0].requestMethod).to.eql(RequestMethod.GET); + expect(paths[1].requestMethod).to.eql(RequestMethod.POST); + expect(paths[2].requestMethod).to.eql(RequestMethod.ALL); + }); + + }); + + describe('exploreMethodMetadata', () => { + + it('should method return expected object which represent single route', () => { + const instance = new TestRoute(); + const instanceProto = Object.getPrototypeOf(instance); + + const route = routerBuilder.exploreMethodMetadata(new TestRoute(), instanceProto, "getTest"); + + expect(route.path).to.eql("/test"); + expect(route.requestMethod).to.eql(RequestMethod.GET); + }); + + }); + + describe('applyPathsToRouterProxy', () => { + + it('should method return expected object which represent single route', () => { + const bindStub = sinon.stub(routerBuilder, "bindMethodToRouterProxy"); + const paths = [ null, null ]; + + routerBuilder.applyPathsToRouterProxy(null, paths); + + expect(bindStub.calledWith(null, null)).to.be.true; + expect(bindStub.callCount).to.be.eql(paths.length); + }); + + }); + +}); \ No newline at end of file diff --git a/src/core/test/router/router-proxy.spec.ts b/src/core/test/router/router-proxy.spec.ts new file mode 100644 index 000000000..ff62977f5 --- /dev/null +++ b/src/core/test/router/router-proxy.spec.ts @@ -0,0 +1,47 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { RouterProxy } from "../../router/router-proxy"; +import { ExceptionsHandler } from "../../exceptions/exceptions-handler"; +import { Exception } from "../../exceptions/exception"; + +describe('RouterProxy', () => { + let routerProxy: RouterProxy; + let handlerMock: sinon.SinonMock; + + beforeEach(() => { + const handler = new ExceptionsHandler(); + handlerMock = sinon.mock(handler); + routerProxy = new RouterProxy(handler); + }); + + describe('createProxy', () => { + + it('should method return thunk', () => { + const proxy = routerProxy.createProxy(() => {}); + expect(typeof proxy === "function").to.be.true; + }); + + it('should method encapsulate callback passed as argument', () => { + const expectation = handlerMock.expects("next").once(); + const proxy = routerProxy.createProxy((req, res, next) => { + throw new Exception("test", 500); + }); + proxy(null, null, null); + expectation.verify(); + }); + + it('should method encapsulate async callback passed as argument', (done) => { + const expectation = handlerMock.expects("next").once(); + const proxy = routerProxy.createProxy(async (req, res, next) => { + throw new Exception("test", 500); + }); + proxy(null, null, null); + + setTimeout(() => { + expectation.verify(); + done(); + }, 0); + }); + + }); +}); \ No newline at end of file diff --git a/src/core/test/router/routes-resolver.spec.ts b/src/core/test/router/routes-resolver.spec.ts new file mode 100644 index 000000000..7a294a6a0 --- /dev/null +++ b/src/core/test/router/routes-resolver.spec.ts @@ -0,0 +1,49 @@ +import * as sinon from "sinon"; +import { expect } from "chai"; +import { RoutesResolver } from "../../router/routes-resolver"; +import { Controller } from "../../../common/utils/controller.decorator"; +import { RequestMapping } from "../../../common/utils/request-mapping.decorator"; +import { RequestMethod } from "../../../common/enums/request-method.enum"; + +describe('RoutesResolver', () => { + @Controller({ path: "global" }) + class TestRoute { + @RequestMapping({ path: "test" }) + getTest() {} + + @RequestMapping({ path: "another-test", method: RequestMethod.POST }) + anotherTest() {} + } + + let router; + let routesResolver: RoutesResolver; + + before(() => { + router = { + get() {}, + post() {} + }; + }); + + beforeEach(() => { + routesResolver = new RoutesResolver(null, { + createRouter: () => router, + }); + }); + + describe('setupRouters', () => { + + it('should method setup controllers to express application instance', () => { + const routes = new Map(); + routes.set(TestRoute, { instance: new TestRoute() }); + + const use = sinon.spy(); + const applicationMock = { use }; + + routesResolver.setupRouters(routes, applicationMock); + expect(use.calledOnce).to.be.true; + expect(use.calledWith('/global', router)).to.be.true; + }); + + }); +}); \ No newline at end of file diff --git a/src/core/test/scanner.spec.ts b/src/core/test/scanner.spec.ts new file mode 100644 index 000000000..2dc8027cb --- /dev/null +++ b/src/core/test/scanner.spec.ts @@ -0,0 +1,72 @@ +import * as sinon from "sinon"; +import { DependenciesScanner } from "./../scanner"; +import { NestContainer } from "./../injector/container"; +import { Module } from "../../common/utils/module.decorator"; +import { NestModule } from "../../common/interfaces/nest-module.interface"; +import { Component } from "../../common/utils/component.decorator"; +import { Controller } from "../../common/utils/controller.decorator"; + +describe('DependenciesScanner', () => { + + @Component() class TestComponent {} + @Controller({ path: "" }) class TestRoute {} + + @Module({ + components: [ TestComponent ], + controllers: [ TestRoute ], + exports: [ TestComponent ] + }) + class AnotherTestModule implements NestModule {} + + @Module({ + modules: [ AnotherTestModule ], + components: [ TestComponent ], + controllers: [ TestRoute ], + }) + class TestModule implements NestModule {} + + let scanner: DependenciesScanner; + let mockContainer: sinon.SinonMock; + let container: NestContainer; + + before(() => { + container = new NestContainer(); + mockContainer = sinon.mock(container); + }); + + beforeEach(() => { + scanner = new DependenciesScanner(container); + }); + + afterEach(() => { + mockContainer.restore(); + }); + + it('should "storeModule" call twice (2 modules) container method "addModule"', () => { + const expectation = mockContainer.expects("addModule").twice(); + scanner.scan(TestModule); + expectation.verify(); + }); + + it('should "storeComponent" call twice (2 components) container method "addComponent"', () => { + const expectation = mockContainer.expects("addComponent").twice(); + const stub = sinon.stub(scanner, "storeExportedComponent"); + + scanner.scan(TestModule); + expectation.verify(); + stub.restore(); + }); + + it('should "storeRoute" call twice (2 components) container method "addRoute"', () => { + const expectation = mockContainer.expects("addRoute").twice(); + scanner.scan(TestModule); + expectation.verify(); + }); + + it('should "storeExportedComponent" call once (1 component) container method "addExportedComponent"', () => { + const expectation = mockContainer.expects("addExportedComponent").once(); + scanner.scan(TestModule); + expectation.verify(); + }); + +}); \ No newline at end of file diff --git a/src/errors/exception-handler.ts b/src/errors/exception-handler.ts new file mode 100644 index 000000000..a7482ef6e --- /dev/null +++ b/src/errors/exception-handler.ts @@ -0,0 +1,16 @@ +import * as clc from "cli-color"; +import { RuntimeException } from "./exceptions/runtime.exception"; + +export class ExceptionHandler { + handle(e: RuntimeException | Error) { + var error = clc.red.bold; + var warn = clc.xterm(214); + + if (e instanceof RuntimeException) { + console.log(error("[Nest] Runtime error!")); + console.log(warn(e.what())); + } + console.log(error("Stack trace:")); + console.log(e.stack); + } +} \ No newline at end of file diff --git a/src/errors/exceptions-zone.ts b/src/errors/exceptions-zone.ts new file mode 100644 index 000000000..ed5bb533b --- /dev/null +++ b/src/errors/exceptions-zone.ts @@ -0,0 +1,14 @@ +import { ExceptionHandler } from "./exception-handler"; + +export class ExceptionsZone { + private static readonly exceptionHandler = new ExceptionHandler(); + + static run(fn: () => void) { + try { + fn(); + } + catch(e) { + this.exceptionHandler.handle(e); + } + } +} \ No newline at end of file diff --git a/src/errors/exceptions/circular-dependency.exception.ts b/src/errors/exceptions/circular-dependency.exception.ts new file mode 100644 index 000000000..5ef201f09 --- /dev/null +++ b/src/errors/exceptions/circular-dependency.exception.ts @@ -0,0 +1,10 @@ +import { RuntimeException } from "./runtime.exception"; + +export class CircularDependencyException extends RuntimeException { + + constructor(type) { + super(`Can't create instance of ${type}. It is possible ` + + `that you are trying to do circular-dependency A->B, B->A.`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/invalid-middleware-configuration.exception.ts b/src/errors/exceptions/invalid-middleware-configuration.exception.ts new file mode 100644 index 000000000..d26bd43a9 --- /dev/null +++ b/src/errors/exceptions/invalid-middleware-configuration.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class InvalidMiddlewareConfigurationException extends RuntimeException { + + constructor() { + super(`Invalid middleware configuration passed in module "configure()" method.`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/invalid-middleware.exception.ts b/src/errors/exceptions/invalid-middleware.exception.ts new file mode 100644 index 000000000..85f0115cd --- /dev/null +++ b/src/errors/exceptions/invalid-middleware.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class InvalidMiddlewareException extends RuntimeException { + + constructor() { + super(`You are trying to setup middleware without "resolve" method.`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/invalid-module-config.exception.ts b/src/errors/exceptions/invalid-module-config.exception.ts new file mode 100644 index 000000000..e9ae61178 --- /dev/null +++ b/src/errors/exceptions/invalid-module-config.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class InvalidModuleConfigException extends RuntimeException { + + constructor(property: string) { + super(`Invalid property [${property}] in @Module({}) annotation.`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/invalid-path-variable.exception.ts b/src/errors/exceptions/invalid-path-variable.exception.ts new file mode 100644 index 000000000..fdc0fb284 --- /dev/null +++ b/src/errors/exceptions/invalid-path-variable.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class InvalidPathVariableException extends RuntimeException { + + constructor(annotationName: string) { + super(`Invalid path in @${annotationName}!`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/runtime.exception.ts b/src/errors/exceptions/runtime.exception.ts new file mode 100644 index 000000000..1e2188002 --- /dev/null +++ b/src/errors/exceptions/runtime.exception.ts @@ -0,0 +1,9 @@ +export class RuntimeException extends Error { + constructor(private msg: string) { + super(); + } + + what() { + return this.msg; + } +} \ No newline at end of file diff --git a/src/errors/exceptions/unkown-dependencies.exception.ts b/src/errors/exceptions/unkown-dependencies.exception.ts new file mode 100644 index 000000000..2cedd6b38 --- /dev/null +++ b/src/errors/exceptions/unkown-dependencies.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class UnkownDependenciesException extends RuntimeException { + + constructor(type) { + super(`Can't recognize dependencies of ${type}.`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/unkown-export.exception.ts b/src/errors/exceptions/unkown-export.exception.ts new file mode 100644 index 000000000..a688987ef --- /dev/null +++ b/src/errors/exceptions/unkown-export.exception.ts @@ -0,0 +1,10 @@ +import { RuntimeException } from "./runtime.exception"; + +export class UnkownExportException extends RuntimeException { + + constructor() { + super(`You are trying to export unkown component. Maybe ` + + `you forgot to place this one to components list also.`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/unkown-middleware.exception.ts b/src/errors/exceptions/unkown-middleware.exception.ts new file mode 100644 index 000000000..77e32d22d --- /dev/null +++ b/src/errors/exceptions/unkown-middleware.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class UnkownMiddlewareException extends RuntimeException { + + constructor() { + super(`Not recognized middleware - runtime error!`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/unkown-module.exception.ts b/src/errors/exceptions/unkown-module.exception.ts new file mode 100644 index 000000000..477bd1fa0 --- /dev/null +++ b/src/errors/exceptions/unkown-module.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class UnkownModuleException extends RuntimeException { + + constructor() { + super(`Not recognized module - runtime error!`); + } + +} \ No newline at end of file diff --git a/src/errors/exceptions/unkown-request-mapping.exception.ts b/src/errors/exceptions/unkown-request-mapping.exception.ts new file mode 100644 index 000000000..f80b13d4e --- /dev/null +++ b/src/errors/exceptions/unkown-request-mapping.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "./runtime.exception"; + +export class UnkownRequestMappingException extends RuntimeException { + + constructor() { + super(`RequestMapping not defined in @RequestMapping() annotation!`); + } + +} \ No newline at end of file diff --git a/src/index.ts b/src/index.ts new file mode 100644 index 000000000..a0aeeef25 --- /dev/null +++ b/src/index.ts @@ -0,0 +1,10 @@ +/* + * Nest + * Copyright(c) 2017 Kamil Mysliwiec + * www.kamilmysliwiec.com + * MIT Licensed + */ + +export * from './common' +export * from './runner'; +export * from './core'; \ No newline at end of file diff --git a/src/nest/common/enums/index.ts b/src/nest/common/enums/index.ts new file mode 100644 index 000000000..eb3a1fab1 --- /dev/null +++ b/src/nest/common/enums/index.ts @@ -0,0 +1 @@ +export * from "./request-method.enum"; \ No newline at end of file diff --git a/src/nest/common/enums/request-method.enum.ts b/src/nest/common/enums/request-method.enum.ts new file mode 100644 index 000000000..189a85a13 --- /dev/null +++ b/src/nest/common/enums/request-method.enum.ts @@ -0,0 +1,7 @@ +export enum RequestMethod { + GET = 0, + POST, + PUT, + DELETE, + ALL +} \ No newline at end of file diff --git a/src/nest/common/interfaces/controller-metadata.interface.ts b/src/nest/common/interfaces/controller-metadata.interface.ts new file mode 100644 index 000000000..b816bf9aa --- /dev/null +++ b/src/nest/common/interfaces/controller-metadata.interface.ts @@ -0,0 +1,3 @@ +export interface ControllerMetadata { + path?: string; +} diff --git a/src/nest/common/interfaces/controller.interface.ts b/src/nest/common/interfaces/controller.interface.ts new file mode 100644 index 000000000..30fb9c54b --- /dev/null +++ b/src/nest/common/interfaces/controller.interface.ts @@ -0,0 +1 @@ +export interface Controller {} diff --git a/src/nest/core/interfaces/index.ts b/src/nest/common/interfaces/index.ts similarity index 59% rename from src/nest/core/interfaces/index.ts rename to src/nest/common/interfaces/index.ts index 09096e08a..6d7f3f2c4 100644 --- a/src/nest/core/interfaces/index.ts +++ b/src/nest/common/interfaces/index.ts @@ -1,7 +1,8 @@ export * from "./path-props.interface"; -export * from "./app-module.interface"; +export * from "./nest-module.interface"; export * from "./module-props.interface"; export * from "./nest-application.interface"; export * from "./route.interface"; export * from "./component.interface"; -export * from "./route-props.interface"; \ No newline at end of file +export * from "./route-props.interface"; +export * from "./nest-application-factory.interface"; \ No newline at end of file diff --git a/src/nest/common/interfaces/injectable.interface.ts b/src/nest/common/interfaces/injectable.interface.ts new file mode 100644 index 000000000..a9e2b48fa --- /dev/null +++ b/src/nest/common/interfaces/injectable.interface.ts @@ -0,0 +1 @@ +export interface Injectable {} diff --git a/src/nest/common/interfaces/module-metadata.interface.ts b/src/nest/common/interfaces/module-metadata.interface.ts new file mode 100644 index 000000000..2e4a9bd5e --- /dev/null +++ b/src/nest/common/interfaces/module-metadata.interface.ts @@ -0,0 +1,9 @@ +import { NestModule } from "./nest-module.interface"; +import { Controller } from "./controller.interface"; + +export interface ModuleMetadata { + modules?: NestModule[], + components?: any[], + controllers?: Controller[], + exports?: any[], +} diff --git a/src/nest/common/interfaces/nest-application.interface.ts b/src/nest/common/interfaces/nest-application.interface.ts new file mode 100644 index 000000000..c3df139b9 --- /dev/null +++ b/src/nest/common/interfaces/nest-application.interface.ts @@ -0,0 +1,3 @@ +export interface NestApplication { + start: () => void; +} \ No newline at end of file diff --git a/src/nest/common/interfaces/nest-module.interface.ts b/src/nest/common/interfaces/nest-module.interface.ts new file mode 100644 index 000000000..b4fe38f4e --- /dev/null +++ b/src/nest/common/interfaces/nest-module.interface.ts @@ -0,0 +1,5 @@ +import { MiddlewaresBuilder } from "../middlewares/builder"; + +export interface NestModule { + configure?: (router: MiddlewaresBuilder) => MiddlewaresBuilder; +} diff --git a/src/nest/core/interfaces/path-props.interface.ts b/src/nest/common/interfaces/request-mapping-metadata.interface.ts similarity index 58% rename from src/nest/core/interfaces/path-props.interface.ts rename to src/nest/common/interfaces/request-mapping-metadata.interface.ts index 9f9d63d33..0d012a120 100644 --- a/src/nest/core/interfaces/path-props.interface.ts +++ b/src/nest/common/interfaces/request-mapping-metadata.interface.ts @@ -1,4 +1,4 @@ -import { RequestMethod } from "./../enums"; +import { RequestMethod } from "../enums/request-method.enum"; export interface RequestMappingProps { path: string, diff --git a/src/nest/core/utils/component.decorator.ts b/src/nest/common/utils/component.decorator.ts similarity index 100% rename from src/nest/core/utils/component.decorator.ts rename to src/nest/common/utils/component.decorator.ts diff --git a/src/nest/common/utils/controller.decorator.ts b/src/nest/common/utils/controller.decorator.ts new file mode 100644 index 000000000..60719cc89 --- /dev/null +++ b/src/nest/common/utils/controller.decorator.ts @@ -0,0 +1,13 @@ +import "reflect-metadata"; +import { RouteProps } from "../interfaces/route-props.interface"; +import { InvalidPathVariableException } from "../../errors/exceptions/invalid-path-variable.exception"; + +export const Controller = (routeProps: RouteProps): ClassDecorator => { + if (typeof routeProps.path === "undefined") { + throw new InvalidPathVariableException("Controller") + } + + return (target: Object) => { + Reflect.defineMetadata("path", routeProps.path, target); + } +}; \ No newline at end of file diff --git a/src/nest/core/utils/index.ts b/src/nest/common/utils/index.ts similarity index 100% rename from src/nest/core/utils/index.ts rename to src/nest/common/utils/index.ts diff --git a/src/nest/core/utils/module.decorator.ts b/src/nest/common/utils/module.decorator.ts similarity index 87% rename from src/nest/core/utils/module.decorator.ts rename to src/nest/common/utils/module.decorator.ts index 247148f4d..079032dfc 100644 --- a/src/nest/core/utils/module.decorator.ts +++ b/src/nest/common/utils/module.decorator.ts @@ -1,5 +1,5 @@ import "reflect-metadata"; -import { ModuleProps } from "./../interfaces"; +import { ModuleProps } from "./."; export const Module = (filter: ModuleProps): ClassDecorator => { return (target: Object) => { diff --git a/src/nest/core/utils/path.decorator.ts b/src/nest/common/utils/request-mapping.decorator.ts similarity index 75% rename from src/nest/core/utils/path.decorator.ts rename to src/nest/common/utils/request-mapping.decorator.ts index bea859504..a156b8b58 100644 --- a/src/nest/core/utils/path.decorator.ts +++ b/src/nest/common/utils/request-mapping.decorator.ts @@ -1,6 +1,6 @@ import "reflect-metadata"; -import { RequestMappingProps } from "./../interfaces"; -import { RequestMethod } from "./../enums"; +import { RequestMappingProps } from "../interfaces/path-props.interface"; +import { RequestMethod } from "../enums/request-method.enum"; export const RequestMapping = (props: RequestMappingProps): MethodDecorator => { const requestMethod = props.method || RequestMethod.GET; diff --git a/src/nest/core/container.ts b/src/nest/core/injector/container.ts similarity index 70% rename from src/nest/core/container.ts rename to src/nest/core/injector/container.ts index 475032ce3..2678dcacc 100644 --- a/src/nest/core/container.ts +++ b/src/nest/core/injector/container.ts @@ -1,13 +1,13 @@ import "reflect-metadata"; -import { Route, Component, AppModule } from "./interfaces"; +import { Route, Component, NestModule } from "./interfaces"; export class NestContainer { - private readonly modules = new Map(); + private readonly modules = new Map(); - addModule(module) { - if(!this.modules.has(module)) { - this.modules.set(module, { - instance: new module(), + addModule(moduleClass) { + if(!this.modules.has(moduleClass)) { + this.modules.set(moduleClass, { + instance: new moduleClass(), relatedModules: [], components: new Map>(), routes: new Map>(), @@ -16,11 +16,11 @@ export class NestContainer { } } - getModules(): Map { + getModules(): Map { return this.modules; } - addRelatedModule(relatedModule: AppModule, module: AppModule) { + addRelatedModule(relatedModule: NestModule, module: NestModule) { if(this.modules.has(module)) { const storedModule = this.modules.get(module); const related = this.modules.get(relatedModule); @@ -29,7 +29,7 @@ export class NestContainer { } } - addComponent(component: any, module: AppModule) { + addComponent(component: any, module: NestModule) { if(this.modules.has(module)) { const storedModule = this.modules.get(module); storedModule.components.set(component, { @@ -39,7 +39,7 @@ export class NestContainer { } - addExportedComponent(exportedComponent: any, module: AppModule) { + addExportedComponent(exportedComponent: any, module: NestModule) { if(this.modules.has(module)) { const storedModule = this.modules.get(module); if (!storedModule.components.get(exportedComponent)) { @@ -50,7 +50,7 @@ export class NestContainer { } - addRoute(route: Route, module: AppModule) { + addRoute(route: Route, module: NestModule) { if(this.modules.has(module)) { const storedModule = this.modules.get(module); storedModule.routes.set(route, { @@ -61,7 +61,7 @@ export class NestContainer { } -export interface ModuleDependencies extends InstanceWrapper { +export interface ModuleDependencies extends InstanceWrapper { relatedModules: ModuleDependencies[]; components?: Map>; routes?: Map>; diff --git a/src/nest/core/injector/injector.ts b/src/nest/core/injector/injector.ts new file mode 100644 index 000000000..611c8fe0f --- /dev/null +++ b/src/nest/core/injector/injector.ts @@ -0,0 +1,102 @@ +import "reflect-metadata"; +import { ModuleDependencies } from "./container"; +import { Middleware } from "../middlewares/interfaces/middleware.interface"; +import { CircularDependencyException } from "../../errors/exceptions/circular-dependency.exception"; +import { UnkownDependenciesException } from "../../errors/exceptions/unkown-dependencies.exception"; +import { MiddlewareProto } from "../middlewares/interfaces/middleware-proto.interface"; + +export class Injector { + + loadInstanceOfMiddleware( + middlewareType, + collection: Map, + module: ModuleDependencies + ) { + const currentFetchedMiddleware = collection.get(middlewareType); + + if(currentFetchedMiddleware === null) { + this.resolveConstructorParams(middlewareType, module, (argsInstances) => { + collection.set(middlewareType, new middlewareType(...argsInstances)) + }); + } + } + + loadInstanceOfRoute(routeType, module: ModuleDependencies) { + const routes = module.routes; + this.loadInstance(routeType, routes, module); + } + + loadInstanceOfComponent(componentType, module: ModuleDependencies) { + const components = module.components; + this.loadInstance(componentType, components, module); + } + + loadInstance(type, collection, module: ModuleDependencies) { + const currentFetchedInstance = collection.get(type); + + if(currentFetchedInstance.instance === null) { + this.resolveConstructorParams(type, module, (argsInstances) => { + currentFetchedInstance.instance = new type(...argsInstances); + }); + } + } + + private resolveConstructorParams(type, module, callback) { + const constructorParams = Reflect.getMetadata('design:paramtypes', type) || []; + const argsInstances = constructorParams.map((param) => ( + this.resolveSingleParam(type, param, module) + )); + + callback(argsInstances); + } + + private resolveSingleParam(targetType, param, module: ModuleDependencies) { + if (typeof param === "undefined") { + throw new CircularDependencyException(targetType); + } + + return this.resolveComponentInstance(module, param, targetType); + } + + private resolveComponentInstance(module: ModuleDependencies, param, componentType) { + const components = module.components; + const instanceWrapper = this.scanForComponent(components, param, module, componentType); + + if (instanceWrapper.instance === null) { + this.loadInstanceOfComponent(param, module); + } + return instanceWrapper.instance; + } + + private scanForComponent(components, param, module, componentType) { + if (!components.has(param)) { + const instanceWrapper = this.scanForComponentInRelatedModules(module, param); + + if (instanceWrapper === null) { + throw new UnkownDependenciesException(componentType); + } + return instanceWrapper; + } + return components.get(param); + } + + private scanForComponentInRelatedModules(module: ModuleDependencies, componentType) { + const relatedModules = module.relatedModules; + let component = null; + + relatedModules.forEach((relatedModule) => { + const { components, exports } = relatedModule; + + if (!exports.has(componentType) || !components.has(componentType)) { + return; + } + + component = components.get(componentType); + if (component.instance === null) { + this.loadInstanceOfComponent(componentType, relatedModule); + } + }); + return component; + } + +} \ No newline at end of file diff --git a/src/nest/core/injector.ts b/src/nest/core/injector/instance-loader.ts similarity index 67% rename from src/nest/core/injector.ts rename to src/nest/core/injector/instance-loader.ts index 5ba481d39..e3c91a3ce 100644 --- a/src/nest/core/injector.ts +++ b/src/nest/core/injector/instance-loader.ts @@ -1,9 +1,8 @@ -import "reflect-metadata"; import { NestContainer, ModuleDependencies } from "./container"; -import { NestInstanceLoader } from "./instance-loader"; +import { Injector } from "./instance-loader"; -export class NestInjector { - private instanceLoader = new NestInstanceLoader(); +export class InstanceLoader { + private injector = new Injector(); constructor(private container: NestContainer) {} @@ -17,13 +16,13 @@ export class NestInjector { private createInstancesOfComponents(module: ModuleDependencies) { module.components.forEach((wrapper, componentType) => { - this.instanceLoader.loadInstanceOfComponent(componentType, module); + this.injector.loadInstanceOfComponent(componentType, module); }); } private createInstancesOfRoutes(module: ModuleDependencies) { module.routes.forEach((wrapper, routeType) => { - this.instanceLoader.loadInstanceOfRoute(routeType, module); + this.injector.loadInstanceOfRoute(routeType, module); }); } diff --git a/src/nest/core/instance-loader.ts b/src/nest/core/instance-loader.ts deleted file mode 100644 index fd56ae763..000000000 --- a/src/nest/core/instance-loader.ts +++ /dev/null @@ -1,123 +0,0 @@ -import "reflect-metadata"; -import { Route } from "./interfaces"; -import { InstanceWrapper, ModuleDependencies } from "./container"; -import { Component } from "./interfaces/component.interface"; -import { Middleware } from "./middlewares/builder"; - -export class NestInstanceLoader { - - loadInstanceOfMiddleware(middlewareType, collection: Map, module: ModuleDependencies) { - const currentFetchedMiddleware = collection.get(middlewareType); - - if(currentFetchedMiddleware === null) { - const argsInstances = []; - const constructorParams = Reflect.getMetadata('design:paramtypes', middlewareType) || []; - - constructorParams.map((param) => { - if (typeof param === "undefined") { - const msg = `Can't create instance of ${middlewareType} ` - + `It is possible that you are trying to do circular-dependency A->B, B->A.`; - - throw new Error(msg); - } - - const componentType = this.resolveComponentInstance(module, param, middlewareType); - argsInstances.push(componentType); - }); - collection.set(middlewareType, new middlewareType(...argsInstances)) - } - } - - loadInstanceOfRoute(routeType, module: ModuleDependencies) { - const { routes } = module; - - const currentFetchedRoute = routes.get(routeType); - - if(currentFetchedRoute.instance === null) { - const argsInstances = []; - const constructorParams = Reflect.getMetadata('design:paramtypes', routeType) || []; - - constructorParams.map((param) => { - if (typeof param === "undefined") { - const msg = `Can't create instance of ${routeType} ` - + `It is possible that you are trying to do circular-dependency A->B, B->A.`; - - throw new Error(msg); - } - - const componentType = this.resolveComponentInstance(module, param, routeType); - argsInstances.push(componentType); - }); - currentFetchedRoute.instance = new routeType(...argsInstances); - } - } - - private resolveComponentInstance(module: ModuleDependencies, param, componentType) { - const components = module.components; - const instanceWrapper = this.scanForComponent(components, param, module, componentType); - - if (instanceWrapper.instance === null) { - this.loadInstanceOfComponent(param, module); - } - return instanceWrapper.instance; - } - - private scanForComponent(components, param, module, componentType) { - let instanceWrapper = null; - - if (!components.has(param)) { - instanceWrapper = this.scanForComponentInRelatedModules(module, param); - - if (instanceWrapper === null) { - throw new Error(`Can't recognize dependencies of ` + componentType); - } - } - else { - instanceWrapper = components.get(param); - } - return instanceWrapper; - } - - public loadInstanceOfComponent(componentType, module: ModuleDependencies) { - const { components } = module; - const currentFetchedComponentInstance = components.get(componentType); - - if(currentFetchedComponentInstance.instance === null) { - const argsInstances = []; - const constructorParams = Reflect.getMetadata('design:paramtypes', componentType) || []; - - constructorParams.map((param) => { - if (typeof param === "undefined") { - const msg = `Can't create instance of ${componentType} ` - + `It is possible that you are trying to do circular-dependency A->B, B->A.`; - - throw new Error(msg); - } - - const instance = this.resolveComponentInstance(module, param, componentType); - argsInstances.push(instance); - }); - currentFetchedComponentInstance.instance = new componentType(...argsInstances); - } - } - - private scanForComponentInRelatedModules(module: ModuleDependencies, componentType) { - const relatedModules = module.relatedModules; - let component = null; - - relatedModules.forEach((relatedModule) => { - const components = relatedModule.components; - const exports = relatedModule.exports; - - if (exports.has(componentType) && components.has(componentType)) { - component = components.get(componentType); - - if (component.instance === null) { - this.loadInstanceOfComponent(componentType, relatedModule); - } - } - }); - return component; - } - -} \ No newline at end of file diff --git a/src/nest/core/interfaces/app-module.interface.ts b/src/nest/core/interfaces/app-module.interface.ts deleted file mode 100644 index d6d8156bc..000000000 --- a/src/nest/core/interfaces/app-module.interface.ts +++ /dev/null @@ -1 +0,0 @@ -export interface AppModule {} diff --git a/src/nest/core/interfaces/component.interface.ts b/src/nest/core/interfaces/component.interface.ts deleted file mode 100644 index 9c0be089d..000000000 --- a/src/nest/core/interfaces/component.interface.ts +++ /dev/null @@ -1 +0,0 @@ -export interface Component {} diff --git a/src/nest/core/interfaces/module-props.interface.ts b/src/nest/core/interfaces/module-props.interface.ts deleted file mode 100644 index 2077d21a7..000000000 --- a/src/nest/core/interfaces/module-props.interface.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { AppModule, Route } from "./"; - -export interface ModuleProps { - modules?: AppModule[], - components?: any[], - routes?: Route[], - exports?: any[], - -} diff --git a/src/nest/core/interfaces/route-props.interface.ts b/src/nest/core/interfaces/route-props.interface.ts deleted file mode 100644 index ec509132f..000000000 --- a/src/nest/core/interfaces/route-props.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface RouteProps { - path: string, -} diff --git a/src/nest/core/interfaces/route.interface.ts b/src/nest/core/interfaces/route.interface.ts deleted file mode 100644 index 6f2a20f9e..000000000 --- a/src/nest/core/interfaces/route.interface.ts +++ /dev/null @@ -1 +0,0 @@ -export interface Route {} diff --git a/src/nest/core/middlewares/middlewares-module.ts b/src/nest/core/middlewares/middlewares-module.ts new file mode 100644 index 000000000..bfba46be3 --- /dev/null +++ b/src/nest/core/middlewares/middlewares-module.ts @@ -0,0 +1,66 @@ +import { Application } from "express"; +import { NestContainer, ModuleDependencies } from "../injector/container"; +import { MiddlewaresBuilder } from "./builder"; +import { MiddlewaresContainer } from "./container"; +import { MiddlewaresResolver } from "./resolver"; +import { RouteProps } from "../../common/interfaces/route-props.interface"; +import { NestModule } from "../../common/interfaces/nest-module.interface"; +import { errorsMsg } from "../errors/error-messages"; +import { MiddlewareConfiguration } from "./interfaces/middleware-configuration.interface"; +import { Middleware } from "./interfaces/middleware.interface"; + +export class MiddlewaresModule { + private static container = new MiddlewaresContainer(); + private static resolver: MiddlewaresResolver; + + static setup(container: NestContainer) { + const modules = container.getModules(); + this.resolver = new MiddlewaresResolver(this.container); + this.resolveMiddlewares(modules); + } + + private static resolveMiddlewares(modules: Map) { + modules.forEach((module) => { + const instance = module.instance; + + this.loadConfiguration(instance); + this.resolver.resolveInstances(module); + }); + } + + static setupMiddlewares(app: Application) { + const configs = this.container.getConfigs(); + + configs.map((config: MiddlewareConfiguration) => { + config.forRoutes.map((route: RouteProps) => { + this.setupRouteMiddleware(route, config, app); + }); + }); + } + + private static loadConfiguration(instance) { + if (!instance["configure"]) { + return; + } + + const middlewaresBuilder = instance.configure(new MiddlewaresBuilder()); + if (middlewaresBuilder) { + const config = middlewaresBuilder.build(); + this.container.addConfig(config); + } + } + + private static setupRouteMiddleware(route: RouteProps, config: MiddlewareConfiguration, app: Application) { + const path = route.path; + + (config.middlewares).map((middlewareType) => { + const middlewaresCollection = this.container.getMiddlewares(); + const middleware = middlewaresCollection.get(middlewareType); + + if (!middleware) { + throw new Error(errorsMsg.unkownMiddleware); + } + app.use(path, middleware.resolve()); + }); + } +} \ No newline at end of file diff --git a/src/nest/core/middlewares/module.ts b/src/nest/core/middlewares/module.ts deleted file mode 100644 index 170f58edb..000000000 --- a/src/nest/core/middlewares/module.ts +++ /dev/null @@ -1,58 +0,0 @@ -import "reflect-metadata"; -import { Express } from "express"; -import { NestContainer } from "../container"; -import { MiddlewaresBuilder, MiddlewareConfiguration, Middleware } from "./builder"; -import { MiddlewaresContainer } from "./container"; -import { MiddlewaresResolver } from "./resolver"; -import { RouteProps } from "../interfaces/route-props.interface"; - -export class MiddlewaresModule { - private static container = new MiddlewaresContainer(); - private static resolver: MiddlewaresResolver; - - static setup(container: NestContainer) { - const modules = container.getModules(); - - this.resolver = new MiddlewaresResolver(this.container, container); - - modules.forEach((module) => { - const instance = module.instance; - - this.loadConfiguration(instance); - this.resolver.resolveInstances(module); - }); - } - - static loadConfiguration(instance) { - if (!instance["configure"]) { - return; - } - - const middlewaresBuilder = instance.configure(new MiddlewaresBuilder()); - if (middlewaresBuilder) { - const config = middlewaresBuilder.build(); - this.container.addConfig(config); - } - } - - static setupMiddlewares(app: Express) { - const configs = this.container.getConfigs(); - - configs.map((config: MiddlewareConfiguration) => { - config.forRoutes.map((route: RouteProps) => { - const path = route.path; - - (config.middlewares).map((middlewareType) => { - const middlewaresCollection = this.container.getMiddlewares(); - const middleware = middlewaresCollection.get(middlewareType); - - if (!middleware) { - throw new Error("Runtime error!"); - } - - app.use(path, middleware.resolve()); - }); - }); - }); - } -} \ No newline at end of file diff --git a/src/nest/core/router-builder.ts b/src/nest/core/router/router-builder.ts similarity index 97% rename from src/nest/core/router-builder.ts rename to src/nest/core/router/router-builder.ts index 04311c2ca..b7f276408 100644 --- a/src/nest/core/router-builder.ts +++ b/src/nest/core/router/router-builder.ts @@ -1,7 +1,7 @@ import "reflect-metadata"; import { Router, RequestHandler, ErrorRequestHandler } from "express"; -import { Route } from "./interfaces"; -import { RequestMethod } from "./enums"; +import { Route } from "./."; +import { RequestMethod } from "./."; export class RouterBuilder { diff --git a/src/nest/core/routes-resolver.ts b/src/nest/core/router/routes-resolver.ts similarity index 78% rename from src/nest/core/routes-resolver.ts rename to src/nest/core/router/routes-resolver.ts index 7c44f3e90..08a580211 100644 --- a/src/nest/core/routes-resolver.ts +++ b/src/nest/core/router/routes-resolver.ts @@ -1,6 +1,6 @@ -import { Express } from "express"; -import { Route } from "./interfaces"; -import { NestContainer, InstanceWrapper } from "./container"; +import { Application } from "express"; +import { Route } from "./."; +import { NestContainer, InstanceWrapper } from "../container"; import { RouterBuilder } from "./router-builder"; export class NestRoutesResolver { @@ -13,7 +13,7 @@ export class NestRoutesResolver { this.routerBuilder = new RouterBuilder(routerFactory); } - resolve(expressInstance: Express) { + resolve(expressInstance: Application) { const modules = this.container.getModules(); modules.forEach((module) => { @@ -21,7 +21,7 @@ export class NestRoutesResolver { }); } - private setupRouters(routes: Map>, expressInstance: Express) { + private setupRouters(routes: Map>, expressInstance: Application) { routes.forEach(({ instance }, routePrototype: Function) => { const { path, router } = this.routerBuilder.build(instance, routePrototype); diff --git a/src/nest/core/utils/route.decorator.ts b/src/nest/core/utils/route.decorator.ts deleted file mode 100644 index 9ae7348af..000000000 --- a/src/nest/core/utils/route.decorator.ts +++ /dev/null @@ -1,8 +0,0 @@ -import "reflect-metadata"; -import { RouteProps } from "./../interfaces"; - -export const Route = (routeProps: RouteProps): ClassDecorator => { - return (target: Object) => { - Reflect.defineMetadata("path", routeProps.path, target); - } -}; \ No newline at end of file diff --git a/src/nest/runner.ts b/src/nest/runner.ts index c41f2360c..21a81c323 100644 --- a/src/nest/runner.ts +++ b/src/nest/runner.ts @@ -1,27 +1,28 @@ import * as express from "express"; -import { NestApplication, AppModule } from "./core/interfaces"; -import { NestDependenciesScanner } from "./core/scanner"; +import { NestApplication, NestModule } from "./core/interfaces"; +import { DependenciesScanner } from "./core/scanner"; import { NestInjector } from "./core/injector"; import { NestRoutesResolver } from "./core/routes-resolver"; import { NestContainer } from "./core/container"; import { SocketModule } from "./socket/socket-module"; import { MiddlewaresModule } from "./core/middlewares/module"; +import { NestApplicationFactory } from "./core/interfaces"; export class NestRunner { private static container = new NestContainer(); - - private static dependenciesScanner = new NestDependenciesScanner(NestRunner.container); + private static dependenciesScanner = new DependenciesScanner(NestRunner.container); private static injector = new NestInjector(NestRunner.container); private static routesResolver = new NestRoutesResolver(NestRunner.container, express.Router); - static run(appPrototype, module: AppModule) { + static run(applicationClass: NestApplicationFactory, module: NestModule) { + this.initialize(module); + this.setupModules(); + this.startApplication(applicationClass); + } + + private static initialize(module: NestModule) { this.dependenciesScanner.scan(module); this.injector.createInstancesOfDependencies(); - - this.setupModules(); - - const appInstance = this.setupApplication(appPrototype); - appInstance.start(); } private static setupModules() { @@ -29,22 +30,30 @@ export class NestRunner { MiddlewaresModule.setup(NestRunner.container); } - private static setupApplication(app: { new(app): T }): NestApplication { + private static startApplication(applicationClass: NestApplicationFactory) { + const appInstance = this.setupApplication(applicationClass); + appInstance.start(); + } + + private static setupApplication(app: T): NestApplication { try { const expressInstance = express(); const appInstance = new app(expressInstance); - MiddlewaresModule.setupMiddlewares(expressInstance); - this.routesResolver.resolve(expressInstance); + this.setupMiddlewares(expressInstance); + this.setupRoutes(expressInstance); return appInstance; } catch(e) { - throw new Error("Invalid application class passed as parameter."); + throw new Error('Invalid application class passed as parameter.'); } } -} + private static setupMiddlewares(expressInstance: express.Application) { + MiddlewaresModule.setupMiddlewares(expressInstance); + } -interface NestApplicationFactory extends NestApplication { - new (app: express.Express); -} \ No newline at end of file + private static setupRoutes(expressInstance: express.Application) { + this.routesResolver.resolve(expressInstance); + } +} diff --git a/src/nest/socket/sockets-container.ts b/src/nest/socket/container.ts similarity index 100% rename from src/nest/socket/sockets-container.ts rename to src/nest/socket/container.ts diff --git a/src/nest/socket/interfaces/gateway-metadata.interface.ts b/src/nest/socket/interfaces/gateway-metadata.interface.ts new file mode 100644 index 000000000..6c47dd020 --- /dev/null +++ b/src/nest/socket/interfaces/gateway-metadata.interface.ts @@ -0,0 +1,4 @@ +export interface GatewayMetadata { + port?: number, + namespace?: string, +} \ No newline at end of file diff --git a/src/nest/socket/interfaces/gateway-props.interface.ts b/src/nest/socket/interfaces/gateway-props.interface.ts deleted file mode 100644 index d3a0f1215..000000000 --- a/src/nest/socket/interfaces/gateway-props.interface.ts +++ /dev/null @@ -1,3 +0,0 @@ -export interface GatewayProps { - namespace?: string, -} \ No newline at end of file diff --git a/src/runner.ts b/src/runner.ts new file mode 100644 index 000000000..ea0587bdf --- /dev/null +++ b/src/runner.ts @@ -0,0 +1,58 @@ +import { Application } from "express"; +import { NestApplication, NestModule } from "./common/interfaces"; +import { DependenciesScanner } from "./core/scanner"; +import { InstanceLoader } from "./core/injector/instance-loader"; +import { RoutesResolver } from "./core/router/routes-resolver"; +import { NestContainer } from "./core/injector/container"; +import { SocketModule } from "./socket/socket-module"; +import { MiddlewaresModule } from "./core/middlewares/middlewares-module"; +import { NestApplicationFactory } from "./common/interfaces"; +import { ExceptionsZone } from "./errors/exceptions-zone"; +import { ExpressAdapter } from "./core/adapters/express-adapter"; + +export class NestRunner { + private static container = new NestContainer(); + private static dependenciesScanner = new DependenciesScanner(NestRunner.container); + private static instanceLoader = new InstanceLoader(NestRunner.container); + private static routesResolver = new RoutesResolver(NestRunner.container, ExpressAdapter); + + static run(applicationClass: NestApplicationFactory, module: NestModule) { + ExceptionsZone.run(() => { + this.initialize(module); + this.setupModules(); + this.startApplication(applicationClass); + }); + } + + private static initialize(module: NestModule) { + this.dependenciesScanner.scan(module); + this.instanceLoader.createInstancesOfDependencies(); + } + + private static setupModules() { + SocketModule.setup(NestRunner.container); + MiddlewaresModule.setup(NestRunner.container); + } + + private static startApplication(applicationClass: NestApplicationFactory) { + const appInstance = this.setupApplication(applicationClass); + appInstance.start(); + } + + private static setupApplication(app: T): NestApplication { + const expressInstance = ExpressAdapter.create(); + const appInstance = new app(expressInstance); + + this.setupMiddlewares(expressInstance); + this.setupRoutes(expressInstance); + return appInstance; + } + + private static setupMiddlewares(expressInstance: Application) { + MiddlewaresModule.setupMiddlewares(expressInstance); + } + + private static setupRoutes(expressInstance: Application) { + this.routesResolver.resolve(expressInstance); + } +} diff --git a/src/socket/adapters/io-adapter.ts b/src/socket/adapters/io-adapter.ts new file mode 100644 index 000000000..893fa6e44 --- /dev/null +++ b/src/socket/adapters/io-adapter.ts @@ -0,0 +1,13 @@ +import * as io from "socket.io"; + +export class IoAdapter { + + static create(port: number) { + return io(port); + } + + static createWithNamespace(port: number, namespace: string) { + return io(port).of(namespace); + } + +} \ No newline at end of file diff --git a/src/socket/container.ts b/src/socket/container.ts new file mode 100644 index 000000000..ef73d80f7 --- /dev/null +++ b/src/socket/container.ts @@ -0,0 +1,14 @@ +import { SocketServerData, ObservableSocketServer } from "./interfaces"; + +export class SocketsContainer { + private readonly socketSubjects = new Map(); + + getSocketSubjects(namespace: string, port: number): ObservableSocketServer { + return this.socketSubjects.get({ namespace, port }); + } + + storeSocketSubjects(namespace: string, port: number, observableServer: ObservableSocketServer) { + this.socketSubjects.set({ namespace, port }, observableServer); + } + +} diff --git a/src/socket/exceptions/invalid-socket-port.exception.ts b/src/socket/exceptions/invalid-socket-port.exception.ts new file mode 100644 index 000000000..2635d8cd8 --- /dev/null +++ b/src/socket/exceptions/invalid-socket-port.exception.ts @@ -0,0 +1,9 @@ +import { RuntimeException } from "../../errors/exceptions/runtime.exception"; + +export class InvalidSocketPortException extends RuntimeException { + + constructor(port, type) { + super(`Invalid port (${port}) in Gateway ${type}!`); + } + +} \ No newline at end of file diff --git a/src/socket/gateway-metadata-explorer.ts b/src/socket/gateway-metadata-explorer.ts new file mode 100644 index 000000000..e5129449e --- /dev/null +++ b/src/socket/gateway-metadata-explorer.ts @@ -0,0 +1,49 @@ +import { Gateway } from "./interfaces/gateway.interface"; + +export class GatewayMetadataExplorer { + + static explore(instance: Gateway): MessageMappingProperties[] { + const instancePrototype = Object.getPrototypeOf(instance); + return this.scanForHandlersFromPrototype(instance, instancePrototype) + } + + static scanForHandlersFromPrototype(instance: Gateway, instancePrototype): MessageMappingProperties[] { + return Object.getOwnPropertyNames(instancePrototype) + .filter((method) => method !== "constructor" && typeof instancePrototype[method] === "function") + .map((methodName) => this.exploreMethodMetadata(instance, instancePrototype, methodName)) + .filter((mapper) => mapper !== null); + } + + static exploreMethodMetadata(instance, instancePrototype, methodName: string): MessageMappingProperties { + const callbackMethod = instancePrototype[methodName]; + const isMessageMapping = Reflect.getMetadata("__isMessageMapping", callbackMethod); + + if(typeof isMessageMapping === "undefined") { + return null; + } + + const message = Reflect.getMetadata("message", callbackMethod); + return { + targetCallback: (callbackMethod).bind(instance), + message, + }; + } + + static *scanForServerHooks(instance: Gateway): IterableIterator { + for (const propertyKey in instance) { + if (typeof propertyKey === "function") { + continue; + } + const isServer = Reflect.getMetadata("__isSocketServer", instance, String(propertyKey)); + if (typeof isServer !== "undefined") { + yield String(propertyKey); + } + } + } + +} + +export interface MessageMappingProperties { + message: string, + targetCallback: Function, +} \ No newline at end of file diff --git a/src/socket/index.ts b/src/socket/index.ts new file mode 100644 index 000000000..99fd62599 --- /dev/null +++ b/src/socket/index.ts @@ -0,0 +1,2 @@ +export * from './interfaces' +export * from './utils'; \ No newline at end of file diff --git a/src/socket/interfaces/gateway-metadata.interface.ts b/src/socket/interfaces/gateway-metadata.interface.ts new file mode 100644 index 000000000..6c47dd020 --- /dev/null +++ b/src/socket/interfaces/gateway-metadata.interface.ts @@ -0,0 +1,4 @@ +export interface GatewayMetadata { + port?: number, + namespace?: string, +} \ No newline at end of file diff --git a/src/socket/interfaces/gateway.interface.ts b/src/socket/interfaces/gateway.interface.ts new file mode 100644 index 000000000..42c924349 --- /dev/null +++ b/src/socket/interfaces/gateway.interface.ts @@ -0,0 +1,5 @@ +export interface Gateway { + afterInit: (server: any) => void; + handleConnection: (client: any) => void; + handleDisconnect: (client: any) => void; +} \ No newline at end of file diff --git a/src/socket/interfaces/index.ts b/src/socket/interfaces/index.ts new file mode 100644 index 000000000..b5e99906a --- /dev/null +++ b/src/socket/interfaces/index.ts @@ -0,0 +1,4 @@ +export * from "./gateway-metadata.interface"; +export * from "./gateway.interface"; +export * from "./observable-socket-server.interface"; +export * from "./socket-server.interface"; \ No newline at end of file diff --git a/src/socket/interfaces/observable-socket-server.interface.ts b/src/socket/interfaces/observable-socket-server.interface.ts new file mode 100644 index 000000000..7f89f37c5 --- /dev/null +++ b/src/socket/interfaces/observable-socket-server.interface.ts @@ -0,0 +1,9 @@ +import { Subject, ReplaySubject } from "rxjs"; + +export interface ObservableSocketServer { + server: any; + init: ReplaySubject; + connection: Subject; + disconnect: Subject; +} + diff --git a/src/socket/interfaces/socket-server.interface.ts b/src/socket/interfaces/socket-server.interface.ts new file mode 100644 index 000000000..87a884460 --- /dev/null +++ b/src/socket/interfaces/socket-server.interface.ts @@ -0,0 +1,4 @@ +export interface SocketServerData { + port: number; + namespace: string; +} diff --git a/src/socket/observable-socket.ts b/src/socket/observable-socket.ts new file mode 100644 index 000000000..75a30f392 --- /dev/null +++ b/src/socket/observable-socket.ts @@ -0,0 +1,15 @@ +import { Subject, ReplaySubject } from "rxjs"; +import { ObservableSocketServer } from "./interfaces/observable-socket-server.interface"; + +export class ObservableSocket { + + static create(server): ObservableSocketServer { + return { + init: new ReplaySubject(), + connection: new Subject(), + disconnect: new Subject(), + server, + }; + } + +} \ No newline at end of file diff --git a/src/socket/socket-module.ts b/src/socket/socket-module.ts new file mode 100644 index 000000000..fc1ccb605 --- /dev/null +++ b/src/socket/socket-module.ts @@ -0,0 +1,36 @@ +import "reflect-metadata"; +import { NestContainer, InstanceWrapper } from "../core/injector/container"; +import { Gateway } from "./interfaces/gateway.interface"; +import { SocketsContainer } from "./container"; +import { SubjectsController } from "./subjects-controller"; +import { Injectable } from "../common/interfaces/injectable.interface"; +import { SocketServerProvider } from "./socket-server-provider"; + +export class SocketModule { + private static socketsContainer = new SocketsContainer(); + private static subjectsController; + + static setup(container: NestContainer) { + this.subjectsController = new SubjectsController( + new SocketServerProvider(this.socketsContainer)); + + const modules = container.getModules(); + modules.forEach(({ components }) => this.hookGatewaysIntoServers(components)); + } + + static hookGatewaysIntoServers(components: Map>) { + components.forEach(({ instance }, componentType) => { + const metadataKeys = Reflect.getMetadataKeys(componentType); + + if (metadataKeys.indexOf("__isGateway") < 0) { + return; + } + + this.subjectsController.hookGatewayIntoServer( + instance, + componentType + ); + }); + } + +} \ No newline at end of file diff --git a/src/socket/socket-server-provider.ts b/src/socket/socket-server-provider.ts new file mode 100644 index 000000000..38c690cd8 --- /dev/null +++ b/src/socket/socket-server-provider.ts @@ -0,0 +1,42 @@ +import "reflect-metadata"; +import { SocketsContainer } from "./container"; +import { ObservableSocket } from "./observable-socket"; +import { ObservableSocketServer } from "./interfaces/observable-socket-server.interface"; +import { IoAdapter } from "./adapters/io-adapter"; + +export class SocketServerProvider { + + constructor(private socketsContainer: SocketsContainer) {} + + public scanForSocketServer(namespace: string, port: number): ObservableSocketServer { + let observableServer = this.socketsContainer.getSocketSubjects(namespace, port); + + if (!observableServer) { + observableServer = this.createSocketServer(namespace, port); + } + return observableServer; + } + + private createSocketServer(namespace: string, port: number) { + const server = this.getServerOfNamespace(namespace, port); + const observableSocket = ObservableSocket.create(server); + + this.socketsContainer.storeSocketSubjects(namespace, port, observableSocket); + return observableSocket; + } + + private getServerOfNamespace(namespace: string, port: number) { + if (namespace) { + return IoAdapter.createWithNamespace(port, this.validateNamespace(namespace)); + } + return IoAdapter.create(port); + } + + private validateNamespace(namespace: string): string { + if(namespace.charAt(0) !== '/') { + return '/' + namespace; + } + return namespace; + } + +} \ No newline at end of file diff --git a/src/socket/subjects-controller.ts b/src/socket/subjects-controller.ts new file mode 100644 index 000000000..290920ffc --- /dev/null +++ b/src/socket/subjects-controller.ts @@ -0,0 +1,91 @@ +import "reflect-metadata"; +import { Gateway } from "./interfaces/gateway.interface"; +import { Injectable } from "../common/interfaces/injectable.interface"; +import { ObservableSocketServer } from "./interfaces/observable-socket-server.interface"; +import { InvalidSocketPortException } from "./exceptions/invalid-socket-port.exception"; +import { GatewayMetadataExplorer, MessageMappingProperties } from "./gateway-metadata-explorer"; +import { Subject } from "rxjs"; +import { SocketServerProvider } from "./socket-server-provider"; + +export class SubjectsController { + + constructor( + private socketServerProvider: SocketServerProvider) {} + + hookGatewayIntoServer(instance: Gateway, componentType: Injectable) { + const namespace = Reflect.getMetadata("namespace", componentType) || ""; + const port = Reflect.getMetadata("port", componentType) || 80; + + if (!Number.isInteger(port)) { + throw new InvalidSocketPortException(port, componentType); + } + this.subscribeObservableServer(instance, namespace, port); + } + + private subscribeObservableServer(instance: Gateway, namespace: string, port: number) { + const messageHandlers = GatewayMetadataExplorer.explore(instance); + const observableServer = this.socketServerProvider.scanForSocketServer(namespace, port); + + this.hookServerToProperties(instance, observableServer.server); + this.subscribeEvents(instance, messageHandlers, observableServer); + } + + private hookServerToProperties(instance: Gateway, server) { + for (const propertyKey of GatewayMetadataExplorer.scanForServerHooks(instance)) { + Reflect.set(instance, propertyKey, server); + } + } + + private subscribeEvents( + instance: Gateway, + messageHandlers: MessageMappingProperties[], + observableServer: ObservableSocketServer) { + + const { + init, + disconnect, + connection, + server + } = observableServer; + + this.subscribeInitEvent(instance, init); + init.next(server); + + server.on("connection", (client) => { + this.subscribeConnectionEvent(instance, connection); + connection.next(client); + + this.subscribeMessages(messageHandlers, client, instance); + this.subscribeDisconnectEvent(instance, disconnect); + client.on("disconnect", (client) => disconnect.next(client)); + }); + } + + private subscribeInitEvent(instance: Gateway, event: Subject) { + if (instance.afterInit) { + event.subscribe(instance.afterInit.bind(instance)); + } + } + + private subscribeConnectionEvent(instance: Gateway, event: Subject) { + if (instance.handleConnection) { + event.subscribe(instance.handleConnection.bind(instance)); + } + } + + private subscribeDisconnectEvent(instance: Gateway, event: Subject) { + if (instance.handleDisconnect) { + event.subscribe(instance.handleDisconnect.bind(instance)); + } + } + + private subscribeMessages(messageHandlers: MessageMappingProperties[], client, instance: Gateway) { + messageHandlers.map(({ message, targetCallback }) => { + client.on( + message, + targetCallback.bind(instance, client), + ); + }); + } + +} \ No newline at end of file diff --git a/src/socket/test/gateway-metadata-explorer.spec.ts b/src/socket/test/gateway-metadata-explorer.spec.ts new file mode 100644 index 000000000..29f50e49d --- /dev/null +++ b/src/socket/test/gateway-metadata-explorer.spec.ts @@ -0,0 +1,116 @@ +import { expect } from "chai"; +import * as sinon from "sinon"; +import { Middleware } from "../../middlewares/interfaces/middleware.interface"; +import { Component } from "../../../common/utils/component.decorator"; +import { MiddlewareBuilder } from "../../middlewares/builder"; +import { MiddlewaresModule } from "../../middlewares/middlewares-module"; +import { UnkownMiddlewareException } from "../../../errors/exceptions/unkown-middleware.exception"; +import { InvalidMiddlewareException } from "../../../errors/exceptions/invalid-middleware.exception"; +import { RequestMethod } from "../../../common/enums/request-method.enum"; +import { Controller } from "../../../common/utils/controller.decorator"; +import { RequestMapping } from "../../../common/utils/request-mapping.decorator"; + +describe('MiddlewaresModule', () => { + @Controller({ path: "test" }) + class AnotherRoute { } + + @Controller({ path: "test" }) + class TestRoute { + + @RequestMapping({ path: "test" }) + getTest() {} + + @RequestMapping({ path: "another", method: RequestMethod.DELETE }) + getAnother() {} + } + + @Component() + class TestMiddleware implements Middleware { + resolve() { + return (req, res, next) => {} + } + } + + describe('loadConfiguration', () => { + + it('should call "configure" method if method is implemented', () => { + const configureSpy = sinon.spy(); + const mockModule = { + configure: configureSpy + }; + + MiddlewaresModule.loadConfiguration(mockModule, "Test"); + + expect(configureSpy.calledOnce).to.be.true; + expect(configureSpy.calledWith(new MiddlewareBuilder())).to.be.true; + }); + }); + + describe('setupRouteMiddleware', () => { + + it('should throw "UnkownMiddlewareException" exception when middlewares is not stored in container', () => { + const route = { path: "Test" }; + const configuration = { + middlewares: [ TestMiddleware ], + forRoutes: [ TestRoute ] + }; + + const useSpy = sinon.spy(); + const app = { use: useSpy }; + + expect(MiddlewaresModule.setupRouteMiddleware.bind( + MiddlewaresModule, route, configuration, "Test", app + )).throws(UnkownMiddlewareException); + }); + + it('should throw "InvalidMiddlewareException" exception when middlewares does not have "resolve" method', () => { + @Component() + class InvalidMiddleware {} + + const route = { path: "Test" }; + const configuration = { + middlewares: [ InvalidMiddleware ], + forRoutes: [ TestRoute ] + }; + + const useSpy = sinon.spy(); + const app = { use: useSpy }; + + const container = MiddlewaresModule.getContainer(); + const moduleKey = "Test"; + container.addConfig([ configuration ], moduleKey); + + const instance = new InvalidMiddleware(); + container.getMiddlewares(moduleKey).set(InvalidMiddleware, instance); + + expect(MiddlewaresModule.setupRouteMiddleware.bind( + MiddlewaresModule, route, configuration, moduleKey, app + )).throws(InvalidMiddlewareException); + }); + + it('should store middlewares when middleware is stored in container', () => { + const route = { path: "Test", method: RequestMethod.GET }; + const configuration = { + middlewares: [ TestMiddleware ], + forRoutes: [ { path: "test" }, AnotherRoute, TestRoute ] + }; + + const useSpy = sinon.spy(); + const app = { + get: useSpy + }; + + const container = MiddlewaresModule.getContainer(); + const moduleKey = "Test"; + container.addConfig([ configuration ], moduleKey); + + const instance = new TestMiddleware(); + container.getMiddlewares(moduleKey).set(TestMiddleware, instance); + + MiddlewaresModule.setupRouteMiddleware(route, configuration, moduleKey, app); + expect(useSpy.calledOnce).to.be.true; + }); + + }); + +}); \ No newline at end of file diff --git a/src/socket/test/utils/socket-gateway.decorator.spec.ts b/src/socket/test/utils/socket-gateway.decorator.spec.ts new file mode 100644 index 000000000..6b2402e72 --- /dev/null +++ b/src/socket/test/utils/socket-gateway.decorator.spec.ts @@ -0,0 +1,20 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { SocketGateway } from "../../utils/socket-gateway.decorator"; + +describe('@SocketGateway', () => { + + @SocketGateway({ port: 80, namespace: "/" }) + class TestGateway {} + + it('should decorate type with expected metadata', () => { + const isGateway = Reflect.getMetadata('__isGateway', TestGateway); + const port = Reflect.getMetadata('port', TestGateway); + const namespace = Reflect.getMetadata('namespace', TestGateway); + + expect(isGateway).to.be.eql(true); + expect(port).to.be.eql(80); + expect(namespace).to.be.eql("/"); + }); + +}); \ No newline at end of file diff --git a/src/socket/test/utils/subscribe-message.decorator.spec.ts b/src/socket/test/utils/subscribe-message.decorator.spec.ts new file mode 100644 index 000000000..79f6014ef --- /dev/null +++ b/src/socket/test/utils/subscribe-message.decorator.spec.ts @@ -0,0 +1,20 @@ +import "reflect-metadata"; +import { expect } from "chai"; +import { SubscribeMessage } from "../../utils/subscribe-message.decorator"; + +describe('@SubscribeMessage', () => { + + class TestGateway { + @SubscribeMessage({ value: "filter" }) + static fn() {} + } + + it('should decorate type with expected metadata', () => { + const isMessageMapping = Reflect.getMetadata('__isMessageMapping', TestGateway.fn); + const message = Reflect.getMetadata('message', TestGateway.fn); + + expect(isMessageMapping).to.be.true; + expect(message).to.be.eql("filter"); + }); + +}); \ No newline at end of file diff --git a/src/socket/utils/index.ts b/src/socket/utils/index.ts new file mode 100644 index 000000000..1551be9c6 --- /dev/null +++ b/src/socket/utils/index.ts @@ -0,0 +1,3 @@ +export * from "./socket-gateway.decorator"; +export * from "./subscribe-message.decorator"; +export * from "./socket-server.decorator"; \ No newline at end of file diff --git a/src/socket/utils/socket-gateway.decorator.ts b/src/socket/utils/socket-gateway.decorator.ts new file mode 100644 index 000000000..d2e8b8558 --- /dev/null +++ b/src/socket/utils/socket-gateway.decorator.ts @@ -0,0 +1,11 @@ +import "reflect-metadata"; +import { GatewayMetadata } from "../interfaces"; + +export const SocketGateway = (metadata?: GatewayMetadata): ClassDecorator => { + metadata = metadata || {}; + return (target: Object) => { + Reflect.defineMetadata("__isGateway", true, target); + Reflect.defineMetadata("namespace", metadata.namespace, target); + Reflect.defineMetadata("port", metadata.port, target); + } +}; \ No newline at end of file diff --git a/src/socket/utils/socket-server.decorator.ts b/src/socket/utils/socket-server.decorator.ts new file mode 100644 index 000000000..f40c681f4 --- /dev/null +++ b/src/socket/utils/socket-server.decorator.ts @@ -0,0 +1,6 @@ +import "reflect-metadata"; + +export const SocketServer: PropertyDecorator = (target: Object, propertyKey: string | symbol): void => { + Reflect.set(target, propertyKey, null); + Reflect.defineMetadata("__isSocketServer", true, target, propertyKey); +}; \ No newline at end of file diff --git a/src/socket/utils/subscribe-message.decorator.ts b/src/socket/utils/subscribe-message.decorator.ts new file mode 100644 index 000000000..aec1faae2 --- /dev/null +++ b/src/socket/utils/subscribe-message.decorator.ts @@ -0,0 +1,11 @@ +import "reflect-metadata"; + +const defaultMetadata = { value: "" }; +export const SubscribeMessage = (metadata: { value: string } = defaultMetadata): MethodDecorator => { + return (target, key, descriptor: PropertyDescriptor) => { + Reflect.defineMetadata("__isMessageMapping", true, descriptor.value); + Reflect.defineMetadata("message", metadata.value, descriptor.value); + + return descriptor; + } +}; \ No newline at end of file diff --git a/tests.webpack.js b/tests.webpack.js new file mode 100644 index 000000000..fa708827b --- /dev/null +++ b/tests.webpack.js @@ -0,0 +1,7 @@ +var chai = require('chai'); +var chaiSinon = require('sinon-chai'); + +chai.use(chaiSinon); + +var context = require.context('./src/', true, /.spec\.[jt]sx?$/); +context.keys().forEach(context); \ No newline at end of file diff --git a/tsconfig.json b/tsconfig.json index e3295c8f5..880a97052 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -8,7 +8,8 @@ "emitDecoratorMetadata": true, "experimentalDecorators": true, "target": "es6", - "sourceMap": true + "sourceMap": true, + "allowJs": true }, "exclude": [ "node_modules" diff --git a/tsconfig.prod.json b/tsconfig.prod.json new file mode 100644 index 000000000..aa0c71b06 --- /dev/null +++ b/tsconfig.prod.json @@ -0,0 +1,23 @@ +{ + "compilerOptions": { + "module": "commonjs", + "declaration": true, + "noImplicitAny": false, + "removeComments": false, + "noLib": false, + "emitDecoratorMetadata": true, + "experimentalDecorators": true, + "target": "es6", + "sourceMap": false, + "outDir": "node_modules/nest" + }, + "files": [ + "src/index.ts" + ], + "include": [ + "src/socket/utils/**.ts" + ], + "exclude": [ + "node_modules" + ] +} \ No newline at end of file diff --git a/webpack.config.test.js b/webpack.config.test.js new file mode 100644 index 000000000..834b6edf7 --- /dev/null +++ b/webpack.config.test.js @@ -0,0 +1,39 @@ +const path = require("path"); +const webpack = require('webpack'); + +const excludes = [ + /node_modules/ +]; + +module.exports = { + watch: true, + target: "node", + resolve: { + extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js", ".json"] + }, + plugins: [ + new webpack.DefinePlugin({ + 'process.env.NODE_ENV': '"test"', + 'global': {}, + 'global.GENTLY': false + }), + new webpack.ProvidePlugin({ + Reflect: 'core-js/es7/reflect', + Map: 'core-js/es7/map', + Set: 'core-js/es7/set' + }) + ], + module: { + loaders: [ + { + test: /\.tsx?$/, + loader: 'awesome-typescript-loader', + exclude: excludes + }, + { + test: /\.json$/, + loader: 'json-loader' + } + ] + } +}; \ No newline at end of file