Skip to content

Commit

Permalink
feat(services): adding services
Browse files Browse the repository at this point in the history
Implement Injectable decorator to enable services
  • Loading branch information
Cervantes007 authored and Lino Alejandro Martell Guevara committed Jul 24, 2020
1 parent ac26e97 commit 57d1bae
Show file tree
Hide file tree
Showing 24 changed files with 329 additions and 71 deletions.
1 change: 1 addition & 0 deletions .eslintrc.js
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ module.exports = {
// e.g. "@typescript-eslint/explicit-function-return-type": "off",
'no-unused-labels': 2,
'no-unused-vars': 2,
'@typescript-eslint/no-explicit-any': 0,
},
}
43 changes: 35 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,26 +1,53 @@
> Nodejs framework to build high-performance app in no time
![CI](https://github.com/cervantes007/moonshard/workflows/CI/badge.svg)
[![codecov](https://codecov.io/gh/cervantes007/moonshard/branch/master/graph/badge.svg)](https://codecov.io/gh/cervantes007/moonshard)
[![semantic-release](https://img.shields.io/badge/%20%20%F0%9F%93%A6%F0%9F%9A%80-semantic--release-e10079.svg)](https://github.com/semantic-release/semantic-release)
[![Commitizen friendly](https://img.shields.io/badge/commitizen-friendly-brightgreen.svg)](http://commitizen.github.io/cz-cli/)
![Snyk Vulnerabilities for GitHub Repo](https://img.shields.io/snyk/vulnerabilities/github/cervantes007/moonshard)
![GitHub license](https://img.shields.io/badge/license-MIT-blue.svg)

## Introduction
Easy peacy

#### Installation
MoonShard was created to improve the development phase and bring high-performance in production environments.
It's easy to start, to use and deploy.

## Features:
- Provide CLI and project scaffolding.
- Logs works and rotate file per day.
- Auto generate `api-docs`.
- Typescript first-class support.
- Full tested.
- Easy deployment to classic server, dockers, serverless or wired into microservices.

## Installation

```
yarn add moonshard
yarn create create-moonshard my-app
```

### Why use moonshard:
## Manual Installation

1. Install the npm package
```shell script
yarn add moonshard
```

2. TypeScript configuration:
Make sure you are using TypeScript version 3.3 or higher, and you have enabled the following settings in `tsconfig.json`:
```
"emitDecoratorMetadata": true,
"experimentalDecorators": true,
```

## Documentation

To check out `examples` and docs, visit [moonshard docs](http://github.com/gh/athene).
To check out `guides`, `api documentation` and `examples`, visit [moonshard docs](http://cervantes007.github.io/moonshard/).

## Issues
## New Features and Issues

Please make sure to read the [Issue Reporting Checklist](http://reporting/) before opening an issue.
You can help to improve MoonShard by create issues for [new features](https://github.com/Cervantes007/moonshard/issues/new?assignees=&labels=&template=feature_request.md&title=)
or [reporting bugs](https://github.com/Cervantes007/moonshard/issues/new?assignees=&labels=&template=bug_report.md&title=)

## Changelog

Expand All @@ -30,7 +57,7 @@ Detailed changes for each release are documented in the [release notes](https://
## Contributions

Please make sure to read the `Contributing Guide` before making a pull request.
Thank you to all the people who already contributed to Athena!
Thank you to all the people who already contributed to MoonShard!

#### Guide for Developers

Expand Down
3 changes: 1 addition & 2 deletions __test__/cats.ctrl.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ describe('Cats Controller Test', () => {
method: 'GET',
url: `/cats?limit=${query.limit}`,
})

expect(response.statusCode).toBe(200)
expect(response.payload).toBe(`This action returns all cats (limit: ${query.limit} items)`)
})
Expand All @@ -28,7 +27,7 @@ describe('Cats Controller Test', () => {
const response = await inject({
method: 'POST',
url: `/cats`,
body: cat,
payload: cat,
})

expect(response.statusCode).toBe(200)
Expand Down
26 changes: 26 additions & 0 deletions __test__/injectable.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
import { getIntance, injectableInstance } from '../src/decorators'

describe('Injectable Test', () => {
test('Injectable from instance', () => {
class A {
getName() {
return 'john'
}
}
const a = new A()
injectableInstance(a)

class B {
constructor(public a?: A) {}
}

const b = new B()

if (b.a) {
expect(b.a.getName()).toBe(a.getName())
}

const aInstance = getIntance(A)
expect(aInstance.getName()).toBe(a.getName())
})
})
9 changes: 9 additions & 0 deletions __test__/start.spec.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { start } from '../src'

test('Start app', async () => {
try {
await start(3000)
} catch (e) {
expect(e.code).toBe('FST_ERR_REOPENED_SERVER')
}
})
37 changes: 31 additions & 6 deletions api/cats/cats.ctrl.ts
Original file line number Diff line number Diff line change
@@ -1,31 +1,56 @@
import { Controller, Post, Body, Get, Query, Param, Put, Delete, HttpCode, Reply, Request } from '../../src'
import {
Controller,
Post,
Body,
Get,
Query,
Param,
Put,
Delete,
HttpCode,
Reply,
Request,
Redirect,
Header,
} from '../../src'
import { CatsService } from './cats.service'

@Controller('cats')
export class CatsController {
constructor(public catsService: CatsService) {}

@Header('Access-Control-Allow-Origin', '*')
@Post()
@HttpCode(200)
create(@Body() createCatDto: any) {
create(@Body() createCatDto: any): string {
return `This action adds a new cat ${createCatDto.name}`
}

@Post('/many')
createCats(@Body() createCatDto: any) {
@Header('Content-Type', 'application/json')
createCats(@Body() createCatDto: any): string {
return `This action adds a new cat ${createCatDto.name}`
}

@Get()
findAll(@Query() query: any) {
findAll(@Query() query: any): string {
return `This action returns all cats (limit: ${query.limit} items)`
}

@Get('/redirect')
@Redirect('/')
redirect() {
return 'redirect'
}

@Get('/name/:name')
findByName(@Request() request, @Reply() reply) {
findByName(@Request() request: any, @Reply() reply: any) {
reply.send(`This action returns cat by name ${request.params.name}`)
}

@Get(':id')
findOne(@Param('id') id: string) {
return `This action returns a #${id} cat`
return this.catsService.findOne(id)
}

@Put(':id')
Expand Down
8 changes: 8 additions & 0 deletions api/cats/cats.service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
import { Injectable } from '../../src'

@Injectable()
export class CatsService {
findOne(id: string): string {
return `This action returns a #${id} cat`
}
}
4 changes: 3 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,8 @@
"main": "lib/index.js",
"types": "lib/types/index.d.ts",
"repository": "git@github.com:Cervantes007/moonshard.git",
"description": "Nodejs framework to build high-performance app in no time",
"keywords": ["node", "nodejs", "framework", "typescript", "fast", "blazing", "performance", "decorator"],
"author": "Cervantes",
"license": "MIT",
"scripts": {
Expand All @@ -12,7 +14,7 @@
"commit": "git-cz",
"dev": "ts-node-dev --respawn src/index.ts",
"docs": "yarn docs:generate && yarn docs:build",
"docs:dev": "cp -r vuepress/. docs/ && vuepress dev docs",
"docs:dev": "yarn docs:generate && cp -r vuepress/. docs/ && vuepress dev docs",
"docs:generate": "rimraf docs && typedoc",
"docs:build": "cp -r vuepress/. docs/ && vuepress build docs",
"docs:serve": "yarn docs && serve docs/.vuepress/dist",
Expand Down
27 changes: 18 additions & 9 deletions src/app/app.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,22 @@
import fastify from 'fastify'
import glob from 'glob'
import fastify, { FastifyInstance, FastifyServerOptions } from 'fastify'
import glob, { IOptions as GlobOptions } from 'glob'
import path from 'path'
import chalk from 'chalk'
import { FastifyLoggerInstance } from 'fastify/types/logger'
import http from 'http'
import { InjectOptions, Response as LightMyRequestResponse } from 'light-my-request'

let app

export const getApp = () => app
export const getApp = (): FastifyInstance => app

export const createApp = (options, { pattern, globOptions }: { pattern?: string; globOptions?: any }) => {
export const createApp = <Server extends http.Server, Logger extends FastifyLoggerInstance = FastifyLoggerInstance>(
options?: FastifyServerOptions<Server, Logger>,
{ pattern, globOptions }: { pattern?: string; globOptions?: GlobOptions } = {
pattern: 'src/**/*.@(ctrl|controller).ts',
globOptions: { matchBase: true },
},
): FastifyInstance => {
const _app = fastify(options)
if (!app) {
app = _app
Expand All @@ -16,7 +25,7 @@ export const createApp = (options, { pattern, globOptions }: { pattern?: string;
return app
}

const loadControllers = (pattern = 'src/**/*.@(ctrl|controller).ts', options = { matchBase: true }) => {
const loadControllers = (pattern = 'src/**/*.@(ctrl|controller).ts', options: GlobOptions = { matchBase: true }) => {
process.env.NODE_ENV !== 'test' && console.log(chalk.bold.blue('Loading controllers:'))
glob.sync(pattern, options).forEach(function (file) {
process.env.NODE_ENV !== 'test' && console.log(chalk.green(`-> ${file}`))
Expand All @@ -28,17 +37,17 @@ export interface IAppOptions {
logger?: boolean
}

export const start = async (port = 3000, options: IAppOptions = { logger: true }, globOptions = {}) => {
export const start = async (port = 3000, options: IAppOptions = { logger: true }, globOptions = {}): Promise<void> => {
if (!app) {
createApp(options, globOptions)
}
try {
await app.listen(port)
} catch (err) {
app.log.error(err)
process.exit(1)
throw err
}
}

export const close = () => app.close()
export const inject = (options) => app.inject(options)
export const close = (): Promise<void> => app.close()
export const inject = (options: InjectOptions | string): Promise<LightMyRequestResponse> => app.inject(options)
15 changes: 11 additions & 4 deletions src/decorators/controller.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,26 @@
import 'reflect-metadata'
import { getApp } from '../app'
import { getStatusByMethod, injectParams } from '../utils/utils'
import { VOLANT_ROUTES } from '../utils/constants'
import { MOONSHARD_ROUTES } from '../utils/constants'
import { spellbook } from './injectable'

export const Controller = (options = '/') => (target) => {
interface ControllerOptions {
prefix?: string
}

export const Controller = (options: ControllerOptions | string = '/'): any => (target: any): void => {
const _options = typeof options === 'string' ? { prefix: options } : options
const routes = Reflect.getMetadata(VOLANT_ROUTES, target) || {}
const routes = Reflect.getMetadata(MOONSHARD_ROUTES, target) || {}
const Constructor = spellbook(target)
const instance = new Constructor()
getApp().register(async (server) => {
const _routes: any[] = Object.values(routes)
for (const route of _routes) {
const { paramsMetadata, handler, status, ..._route } = route
const _status = status || getStatusByMethod(route.method)
_route.handler = (request, reply) => {
const params = injectParams({ paramsMetadata, request, reply: reply.status(_status) })
return handler(...params)
return handler.call(instance, ...params)
}
server.route(_route)
}
Expand Down
1 change: 1 addition & 0 deletions src/decorators/index.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
export * from './controller'
export * from './methods'
export * from './params'
export * from './injectable'
32 changes: 32 additions & 0 deletions src/decorators/injectable.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
import 'reflect-metadata'
import { MOONSHARD_INJECTABLE } from '../utils/constants'

export function injectableInstance(instance): void {
Reflect.defineMetadata(MOONSHARD_INJECTABLE, instance, instance.constructor)
}

export function getIntance<T>(target: new () => T): T {
return Reflect.getMetadata(MOONSHARD_INJECTABLE, target)
}

export function Injectable() {
return (target: any) => {
const newConstructor = spellbook(target) || target
Reflect.defineMetadata(MOONSHARD_INJECTABLE, new newConstructor(), newConstructor)
return newConstructor
}
}

export function spellbook(target) {
const paramTypes: any[] = Reflect.getOwnMetadata('design:paramtypes', target)
if (paramTypes) {
const newArgs = paramTypes.map((param) => {
const currentInstance = Reflect.getMetadata(MOONSHARD_INJECTABLE, param)
if (currentInstance) {
return currentInstance
}
})
return target.bind(null, ...newArgs)
}
return Reflect.getMetadata(MOONSHARD_INJECTABLE, target)
}

0 comments on commit 57d1bae

Please sign in to comment.