Commit f9064439 authored by Nicolas Brändli's avatar Nicolas Brändli
Browse files

commit initial

parents
# Editor configuration, see https://editorconfig.org
root = true
[*]
charset = utf-8
end_of_line = lf
indent_style = tab
indent_size = 4
insert_final_newline = true
trim_trailing_whitespace = true
[package.json]
indent_style = space
indent_size = 2
[*.md]
max_line_length = off
trim_trailing_whitespace = false
dist/
node_modules/
{
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "attach",
"name": "Start debug and listen",
"address": "127.0.0.1",
"port": 9229,
"restart": true
}
]
}
# Typescript / Koa project template.
Table of Content
- [Typescript / Koa project template.](#typescript--koa-project-template)
- [Install](#install)
- [NPM Commands](#npm-commands)
- [Template overview](#template-overview)
- [Dependency Injection](#dependency-injection)
- [Services](#services)
- [Middleware](#middleware)
- [Routing](#routing)
- [Controller Options](#controller-options)
- [Attach middlewares](#attach-middlewares)
- [Configurations](#configurations)
- [Load a configuration file](#load-a-configuration-file)
- [Get a configuration](#get-a-configuration)
- [Listen to config changes](#listen-to-config-changes)
- [Applications](#applications)
- [Cron](#cron)
- [Log](#log)
- [Configure log output](#configure-log-output)
- [Example](#example)
- [Controller](#controller)
- [Cron](#cron-1)
## Install
* `npm install -g webpack nodemon`
* `npm install`
## NPM Commands
* `npm run build` => Build the project using the production configuration
* `npm run start:dev` => Start the server using the dev config + watch files.
* `npm run start:debug` => Start the server using the debug config + watch files + remote debbuging.
## Template overview
This project template allow developers to:
* Auto load controller files (`*.controller.ts`, using webpack and typescript plugin)
* Declare services
* Declare middlewares
* Use dependency injection and IoC (available in controllers, services, middlewares, application).
* Declare multiple Koa application
* Declare JSON config files.
The project structure is:
```
src/
app/ # The project app code may be here.
core/ # Boilerplate code
main.ts # Entry point of the project.
webpack/
webpack.config.<env>.js # webpack configuration
dist/
# The bundled code including config files (copy from the src folder at build time)
```
## Dependency Injection
This project template use the tsyringe package to allow developers to use DI and IoC.
## Services
You can declare service using the Service decorator:
```typescript
@Service()
export class MyService {}
```
And use it in other service or controller:
```typescript
@Service()
export class OtherService {
constructor(private myService: MyService) {}
}
```
## Middleware
You can declare a middleware class and use it in controller or route handler.
A middleware class is a singleton and can use DI.
```typescript
import { Context, Next } from "koa";
import { Middleware } from "@decorators/middleware.decorator";
import { MiddlewareInterface } from "@core/middleware.interface";
@Middleware()
export class MyMiddleware implements MiddlewareInterface {
use(ctx: Context, next: Next): any {
/* ... */
}
}
```
You can declare a middleware as application level using the autoloadIn property:
```typescript
// Add the middleware to the main application (default application in the project template).
@Middleware({ autoloadIn: 'main' })
```
## Routing
To declare a Koa route, you must create a `*.controller.ts` file and export an annoted controller class as default:
```typescript
@Controller()
export default class MyController {
@Get('/user/:id')
async getUserById() {/* some code here... */}
}
```
Every `*.controller.ts` in the project are automatically imported in the `main.ts` file.
### Controller Options
You can set options to the controller annotation.
| Option | Description |
| ------ | --------------------------------------------------------- |
| prefix | The prefix apply to each routes defined by the controller |
| app | The application name (default to `main`) |
Example:
```typescript
@Controller({ prefix: '/my_route' })
/*... */
```
(e.g: the prefix must start with a "`/`" if it's provided)
### Attach middlewares
You can attach middlewares to a controller or methods with the `AttachMiddleware` decorator:
```typescript
@AttachMiddleware([/* a list of middleware */])
@Controller()
export default class MyController {}
```
(Note: Middlewares attached to a controller will be called for each controller routes)
```typescript
@Controller()
export default class MyController {
@AttachMiddleware([/* a list of middleware */])
@Get('/my/path')
myPath(ctx: Context) {}
}
```
## Configurations
* You can create several config files at the root of the `src/` folder.
* Each configuration file must be named according to the following pattern: `*.config.json`.
### Load a configuration file
Use the `loadConfigFile` function from the `config.ts` file:
```typescript
import { loadConfigFile } from '@core/config';
loadConfigFile('my_config'); // Note: the expected config file name is "my_config.config.json".
```
### Get a configuration
Simply:
```typescript
import { config } from '@core/config';
config.my_config
```
Or using the config service
```typescript
@Service() // Or any other class that use the IoC (Controller/Middleware/etc...
export class MyService {
constructor(private configService: ConfigService) {
this.configService.my_config
}
}
```
### Listen to config changes
Each config files are watched. The config service allows you to listen to these changes:
```typescript
@Service() // Or any other class that use the IoC (Controller/Middleware/etc...
export class MyService {
constructor(private configService: ConfigService) {
this.configService.change$.on(CONFIG_EVENTS.CHANGE, configName => {
if (configName === 'my_config') {
// Do something with the new config.
}
});
}
}
```
## Applications
The template provide a koa app instance by default, but you can define and run multiple app at the same time.
To create a new koa app, first create a new file called `my_app.app.ts` and export a class:
```typescript
@Application('my_app', MY_APP_PORT)
export default class MyApp extends AbstractApp {}
```
Now you can use your new app on a specific controller :
```typescript
@Controller({ app: 'my_app' })
/*...*/
```
## Cron
You can declare a cron job using the `@Cron()` decorator.
```typescript
import { Cron } from "@decorators/cron.decorator";
@Cron({ cronTime: '* * * * * *' })
```
## Log
This template include the winston package for app logging.
A service is provided:
```typescript
/* ... imports, decorators */
export default class MyClass {
constructor(private logService: LoggerService) {}
someMethod() {
this.logService.logger.info('log with info level');
this.logService.logger.error('log with error level');
/* etc... */
}
}
```
### Configure log output
2 type of output are supported by default:
* Console
* File
The loggerService use the app.config.file for that.
```json
{
"logger": {
"level": "silly",
"transports": [
{ "type": "console" },
{
"type": "file",
"options": {
"filename": "app.log",
"maxsize": 100000,
"maxFiles": 10,
"tailable": true,
"zippedArchive": true
}
}
]
}
}
```
## Example
### Controller
```typescript
import { Controller, Get, Post } from '@core/decorators/controller.decorator';
import { Context } from 'koa';
import { MyService } from '@app/my.service';
import { MyMiddleware } from '@app/my.middleware';
import { Joi } from 'koa-joi-router';
@Controller()
export default class LoremController {
constructor(private myService: MyService) { }
@AttachMiddleware([MyMiddleware])
@Get('/ipsum')
getIpsum(ctx: Context) {
this.myService.fooo();
ctx.body = { message: 'test ipsum dolor sit amet' };
}
@Post('/lorem/:id', {
body: {
name: Joi.string().max(100),
email: Joi.string().lowercase().email()
},
type: 'json'
})
postLorem(ctx: Context) {
ctx.body = ctx.request.body;
ctx.body.id = ctx.request.params.id;
}
}
```
### Cron
```typescript
import { CronJob } from "cron";
import { OnTick, OnInit, OnComplete } from "@core/cron.interface";
import { Cron } from "@decorators/cron.decorator";
@Cron({ cronTime: '* * * * * *' })
export default class SampleCron implements OnInit, OnTick, OnComplete {
/**
* The cron job instance.
* /!\ Not available in the constructor. Use the OnInit interface if you want to start the cron manually.
*/
job: CronJob | undefined;
onInit(): void {
/* ... init code here. */
this.job.start(); // Or use the start property in the decorator options.
setTimeout(() => {
// Stop the job and call the onComplete callback.
this.job?.stop();
}, 5000)
}
onTick(): void {
/* ... */
}
onComplete(): void {
/* ... */
}
}
```
This diff is collapsed.
This diff is collapsed.
{
"name": "affluence-serv",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"build": "webpack --env=prod",
"run:debug": "nodemon --quiet --inspect dist/app.js",
"run:dev": "nodemon --quiet dist/app.js",
"start:dev": "webpack --env=dev",
"start:debug": "webpack --env=debug",
"start": "node --quiet dist/app.js"
},
"author": "",
"license": "ISC",
"dependencies": {
"@koa/cors": "^3.1.0",
"axios": "^0.19.2",
"chokidar": "^3.3.1",
"cron": "^1.8.2",
"koa": "^2.11.0",
"koa-joi-router": "^6.0.2",
"koa-ratelimit": "^4.2.1",
"moment": "^2.24.0",
"pg": "^8.2.0",
"reflect-metadata": "^0.1.13",
"tsyringe": "^4.1.0",
"winston": "^3.2.1"
},
"devDependencies": {
"@types/cron": "^1.7.2",
"@types/koa": "^2.11.2",
"@types/koa-joi-router": "^5.2.3",
"@types/koa-ratelimit": "^4.2.1",
"@types/koa__cors": "^3.0.1",
"@types/moment": "^2.13.0",
"@types/node": "^13.9.4",
"@types/pg": "^7.14.3",
"clean-webpack-plugin": "^3.0.0",
"copy-webpack-plugin": "^5.1.1",
"nodemon": "^2.0.2",
"ts-loader": "^6.2.2",
"ts-transform-auto-require": "^1.1.0",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^3.8.3",
"webpack": "^4.42.1",
"webpack-cli": "^3.3.11",
"webpack-node-externals": "^1.7.2",
"webpack-shell-plugin-next": "^1.1.7"
}
}
{
"port": 3015,
"externalUrl": ["metromobilite.fr","mobilites-m.fr","localhost:4200"],
"logger": {
"level": "silly",
"transports": [
{ "type": "console" },
{
"type": "file",
"options": {
"filename": "affluence.log",
"maxsize": 100000,
"maxFiles": 10,
"tailable": true,
"zippedArchive": true
}
}
]
}
}
import { Context, Next } from 'koa';
import cors from '@koa/cors';
import { Middleware } from "@decorators/middleware.decorator";
import { MiddlewareInterface } from "@core/middleware.interface";
import { config } from '@core/config';
@Middleware({ autoloadIn: 'main' })
export default class CorsMiddleware implements MiddlewareInterface {
async use(ctx: Context, next: Next) {
return await cors({
origin: (ctx) => {
let origin = ctx.header.origin;
if (config.app.externalUrl.indexOf(origin) !== -1) {
return origin;
}
return false;
},
allowMethods: 'GET,HEAD,PUT,POST,DELETE'
})(ctx, next);
}
}
import { CronJob } from "cron";
import { OnTick, OnInit, OnComplete } from "@core/cron.interface";
import { Cron } from "@decorators/cron.decorator";
import { LoggerService } from "@core/logger.service";
import { ClientFactoryService } from "./db/client.factory";
import { config } from '@core/config';
import { loggers } from "winston";
import { UserReportService } from "./user-report/user-report.service";
@Cron({
cronTime: '10 * * * * *',
runOnInit: true,
start: true,
timeZone: 'Europe/Paris'
})
export default class DbCron implements OnTick, OnInit {
constructor(
private userReportService: UserReportService,
private clientFactory: ClientFactoryService,
private log: LoggerService
) { }
async onInit(): Promise<void> {
let client;
try {
client = this.clientFactory.getAffluenceClient();
await client.connect();
await client.query(`CREATE TABLE IF NOT EXISTS ${config.db.affluence.schema}.user_report (stop_code text NOT NULL,time timestamp with time zone NOT NULL,data json, CONSTRAINT user_report_pk PRIMARY KEY(time,stop_code)) WITH (oids = false)`);
client.end();
} catch (e) {
client?.end();
this.log.logger.error(`[cron][DbCron][onInit] ${e}`);
}
}
async onTick(): Promise<void> {
let client;
try {
let data = this.userReportService.getRawReportsAndFlush();
client = this.clientFactory.getAffluenceClient();
await client.connect();
for (let report of data) {
this.log.logger.debug(JSON.stringify(report));
await client.insert('user_report', '(stop_code,time,data)', '($1,$2,$3)', [report.stop_code, report.time, JSON.stringify(report.data)]);
}
client.end();
} catch (e) {
client?.end();
this.log.logger.error(`[cron][DbCron][onTick] ${e}`);
}
}
}
import { Client, QueryResult, QueryConfig } from "pg";
import { config } from '@core/config';
import { LoggerService } from "@core/logger.service";
export class AffluenceClient extends Client {
constructor(private log: LoggerService) {
super(config.db.affluence);
}
async connect(): Promise<void> {
await super.connect();
await this.query(`set search_path = '${config.db.affluence.schema}';`);
}
/*async insertReturning(table: string, columns: string, values: string, params: Array<any>): Promise<number> {
try {
let query = `insert into ${table} ${columns} values ${values} returning ident;`;
this.log.logger.silly(`[client][AffluenceClient][insertReturning] ${query}`);
let result = await this.query<{ ident: number }>(query, params);
if (result.rows.length === 0) throw new Error(`${table} : Unable to perform ${result.command}.`);
return result.rows[0].ident;
} catch (e) {
if (e.details) this.log.logger.error(`[client][AffluenceClient][insertReturning] ${e.details}`);
throw (e);
}
}*/
async insert(table: string, columns: string, values: string, params: Array<any>, name?: string): Promise<boolean> {
try {
let queryConfig: QueryConfig = {
text: `insert into ${table} ${columns} values ${values};`,
values: params
}
if (name) queryConfig.name = name;
this.log.logger.silly(`[client][AffluenceClient][insert] ${queryConfig.text}`);
let result = await this.query<{ ident: number }>(queryConfig);
if (result.rowCount !== 1) throw new Error(`${table} : Unable to perform ${result.command}.`);
return true;
} catch (e) {
if (e.details) this.log.logger.error(`[client][AffluenceClient][insert] ${e.details}`);
throw (e);
}
}
/*async update(table: string, columns: string, values: string, where: string, params: Array<any>) {
try {
let query = `update ${table} set ${columns} = ${values} where 1=1 ${where};`;
this.log.logger.silly(`[client][AffluenceClient][update] ${query}`);
return await this.query(query, params);
} catch (e) {
if (e.details) this.log.logger.error(`[client][AffluenceClient][update] ${e.details}`);
throw (e);
}
}
async delete(table: string, where: string, params: Array<any>) {
try {
let query = `delete from ${table} where 1=1 ${where};`;
this.log.logger.silly(`[client][AffluenceClient][delete] ${query}`);
return await this.query(query, params);
} catch (e) {
if (e.details) this.log.logger.error(`[client][AffluenceClient][delete] ${e.details}`);
throw (e);
}
}*/
}