├── templates ├── js │ ├── base │ │ ├── .gitignore │ │ ├── .dockerignore │ │ ├── Dockerfile │ │ ├── .editorconfig │ │ ├── README.md │ │ ├── package.json │ │ └── .eslintrc │ ├── controllers │ │ ├── index.js │ │ └── main-controller.js │ ├── services │ │ ├── index.js │ │ ├── mongo.js │ │ └── rabbitmq.js │ ├── index.js │ ├── app │ │ ├── router.js │ │ └── server.js │ └── models │ │ └── contact.js ├── ts │ ├── base │ │ ├── .dockerignore │ │ ├── .gitignore │ │ ├── Dockerfile │ │ ├── .editorconfig │ │ ├── tsconfig.json │ │ ├── package.json │ │ └── .eslintrc │ ├── controllers │ │ ├── index.ts │ │ └── main-controller.ts │ ├── app │ │ ├── router.ts │ │ └── server.ts │ ├── index.ts │ ├── models │ │ └── contact.ts │ └── services │ │ ├── mongo.ts │ │ └── rabbitmq.ts └── common │ └── swagger.json ├── bin └── node-poundcake ├── .gitignore ├── .editorconfig ├── package.json ├── LICENSE ├── README.md ├── .eslintrc └── lib └── cli.js /templates/js/base/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn.lock 3 | -------------------------------------------------------------------------------- /templates/ts/base/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .git 3 | dist 4 | -------------------------------------------------------------------------------- /bin/node-poundcake: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | require('../lib/cli.js'); 3 | -------------------------------------------------------------------------------- /templates/ts/base/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | node_modules 3 | .env 4 | yarn.lock 5 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | package-lock.json 3 | yarn.lock 4 | yarn-error.log 5 | -------------------------------------------------------------------------------- /templates/js/base/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .gitignore 3 | npm-debug.log 4 | package-lock.json -------------------------------------------------------------------------------- /templates/js/controllers/index.js: -------------------------------------------------------------------------------- 1 | const MainController = require('./main-controller'); 2 | 3 | exports.mainController = new MainController(); 4 | -------------------------------------------------------------------------------- /templates/ts/controllers/index.ts: -------------------------------------------------------------------------------- 1 | import MainController from './main-controller'; 2 | 3 | export default { 4 | mainController: new MainController() 5 | }; 6 | -------------------------------------------------------------------------------- /templates/js/services/index.js: -------------------------------------------------------------------------------- 1 | const MongoService = require('./mongo-service'); 2 | const RabbitMqService = require('./rabbitmq-service'); 3 | 4 | exports.mongo = new MongoService(); 5 | exports.rabbitmq = new RabbitMqService(); 6 | -------------------------------------------------------------------------------- /templates/js/index.js: -------------------------------------------------------------------------------- 1 | require('dotenv').config(); 2 | 3 | (async() => { 4 | try { 5 | console.log('app start'); 6 | } catch (e) { 7 | console.error(e); 8 | process.exit(1); 9 | } 10 | })(); 11 | -------------------------------------------------------------------------------- /templates/ts/app/router.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import controllers from '../controllers'; 3 | 4 | const router = express.Router(); 5 | 6 | router.get('/ping', controllers.mainController.ping); 7 | 8 | export default router; 9 | -------------------------------------------------------------------------------- /templates/js/app/router.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | 4 | const { mainController } = require('../controllers'); 5 | 6 | router.get('/ping', mainController.ping); 7 | 8 | module.exports = router; 9 | -------------------------------------------------------------------------------- /templates/ts/index.ts: -------------------------------------------------------------------------------- 1 | import 'dotenv/config'; 2 | 3 | (async(): Promise => { 4 | try { 5 | console.log('app start'); 6 | } catch (error) { 7 | console.error(error); 8 | process.exit(1); 9 | } 10 | })(); 11 | -------------------------------------------------------------------------------- /templates/ts/models/contact.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | const ContactSchema = mongoose.Schema({ 4 | email: String 5 | }); 6 | 7 | ContactSchema.index({ email: -1 }); 8 | 9 | export default mongoose.model('Contact', ContactSchema); 10 | -------------------------------------------------------------------------------- /templates/js/models/contact.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | const ContactSchema = mongoose.Schema({ 4 | email: String 5 | }); 6 | 7 | ContactSchema.index({ email: -1 }); 8 | 9 | module.exports = mongoose.model('Contact', ContactSchema); 10 | -------------------------------------------------------------------------------- /templates/js/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:12 2 | 3 | WORKDIR /src 4 | 5 | COPY package.json /src 6 | RUN npm install 7 | ADD . . 8 | 9 | RUN apk add --update tzdata 10 | ENV TZ=America/Sao_Paulo 11 | RUN rm -rf /var/cache/apk/* 12 | 13 | CMD ["npm", "start"] 14 | -------------------------------------------------------------------------------- /templates/ts/base/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM mhart/alpine-node:12 2 | 3 | WORKDIR /src 4 | 5 | COPY package.json /src 6 | RUN npm install 7 | 8 | ADD . . 9 | 10 | RUN apk add --update tzdata 11 | ENV TZ=America/Recife 12 | RUN rm -rf /var/cache/apk/* 13 | 14 | CMD ["npm", "start:prod"] 15 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.js, **/*.js] 9 | indent_size = 4 10 | indent_style = space 11 | 12 | [{package.json}, {.eslintrc}] 13 | indent_size = 4 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /templates/ts/controllers/main-controller.ts: -------------------------------------------------------------------------------- 1 | import { Request, Response } from 'express'; 2 | 3 | class MainController { 4 | ping(request: Request, response: Response): Response { 5 | return response.status(200).send('pong'); 6 | } 7 | } 8 | 9 | export default MainController; 10 | -------------------------------------------------------------------------------- /templates/js/base/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.js, **/*.js] 9 | indent_size = 4 10 | indent_style = space 11 | 12 | [{package.json}, {.eslintrc}] 13 | indent_size = 4 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /templates/ts/base/.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | end_of_line = lf 5 | insert_final_newline = true 6 | trim_trailing_whitespace = true 7 | 8 | [*.js, **/*.js] 9 | indent_size = 4 10 | indent_style = space 11 | 12 | [{package.json}, {.eslintrc}] 13 | indent_size = 4 14 | indent_style = space 15 | -------------------------------------------------------------------------------- /templates/js/controllers/main-controller.js: -------------------------------------------------------------------------------- 1 | class MainController { 2 | ping(req, res) { 3 | try { 4 | return res.status(200).send('pong'); 5 | } catch (e) { 6 | return res.status(500).send('server error'); 7 | } 8 | } 9 | } 10 | 11 | module.exports = MainController; 12 | -------------------------------------------------------------------------------- /templates/ts/base/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "outDir": "dist", 4 | "module": "commonjs", 5 | "target": "es2019", 6 | "esModuleInterop": true, 7 | "sourceMap": true, 8 | "rootDir": "src", 9 | "noImplicitAny": true, 10 | "suppressImplicitAnyIndexErrors": true 11 | }, 12 | "include": [ 13 | "src" 14 | ], 15 | "exclude": [ 16 | "**/*.spec.ts", 17 | "**/*.test.ts" 18 | ] 19 | } -------------------------------------------------------------------------------- /templates/js/base/README.md: -------------------------------------------------------------------------------- 1 | ## Setup 2 | 3 | Install dependencies: 4 | 5 | ```bash 6 | $ yarn install 7 | ``` 8 | 9 | Start app: 10 | 11 | ```bash 12 | $ yarn start 13 | ``` 14 | 15 | Run on development: 16 | 17 | ```bash 18 | $ yarn start:dev 19 | ``` 20 | 21 | Run on Docker: 22 | 23 | ```bash 24 | $ bash docker.sh start 25 | ``` 26 | 27 | Run tests: 28 | 29 | ```bash 30 | $ yarn test 31 | ``` 32 | 33 | Run linter: 34 | 35 | ```bash 36 | $ yarn lint 37 | ``` 38 | -------------------------------------------------------------------------------- /templates/js/app/server.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = require('./router'); 3 | 4 | class Server { 5 | constructor() { 6 | this.app = express(); 7 | this.port = process.env.PORT || 8000; 8 | this.app.use(express.json()); 9 | this.app.use('/', router); 10 | } 11 | 12 | listen() { 13 | this.app.listen(this.port, () => console.log(`listen at ${this.port}`)); 14 | } 15 | } 16 | 17 | module.exports = Server; 18 | -------------------------------------------------------------------------------- /templates/ts/app/server.ts: -------------------------------------------------------------------------------- 1 | import express from 'express'; 2 | import router from './router'; 3 | 4 | export default class Server { 5 | app: express.Application; 6 | port: number; 7 | 8 | constructor() { 9 | this.app = express(); 10 | this.port = 8000; 11 | this.app.use(express.json()); 12 | this.app.use('/', router); 13 | } 14 | 15 | listen(): void { 16 | this.app.listen(this.port, () => console.log(`server listen at ${this.port}`)); 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-poundcake", 3 | "version": "1.2.2", 4 | "description": "", 5 | "main": "src/cli.js", 6 | "scripts": { 7 | "start": "node lib/cli.js", 8 | "test": "echo \"Error: no test specified\" && exit 1" 9 | }, 10 | "bin": { 11 | "node-poundcake": "./bin/node-poundcake" 12 | }, 13 | "keywords": [ 14 | "node", 15 | "typescript", 16 | "mongoose", 17 | "amqplib", 18 | "scaffold", 19 | "boilerplate", 20 | "api", 21 | "express" 22 | ], 23 | "author": "Thiago Moraes", 24 | "license": "MIT", 25 | "dependencies": { 26 | "commander": "^6.2.0" 27 | }, 28 | "repository": { 29 | "type": "git", 30 | "url": "https://github.com/thiagomr/node-poundcake" 31 | }, 32 | "devDependencies": { 33 | "eslint": "^7.12.0" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /templates/ts/services/mongo.ts: -------------------------------------------------------------------------------- 1 | import mongoose from 'mongoose'; 2 | 3 | class Mongo { 4 | url: string 5 | 6 | constructor(url: string) { 7 | this.url = url; 8 | 9 | mongoose.connection.on('connected', () => console.info(`[mongo] connected at ${this.url}`)); 10 | mongoose.connection.on('disconnected', (e: Error) => console.info('[mongo] disconnected', e)); 11 | mongoose.connection.on('error', (e: Error) => console.info('[mongo] error', e)); 12 | } 13 | 14 | async connect(): Promise { 15 | await mongoose.connect(this.url, { 16 | useNewUrlParser: true, 17 | useUnifiedTopology: true 18 | }); 19 | } 20 | 21 | async disconnect(): Promise { 22 | await mongoose.disconnect(); 23 | } 24 | } 25 | 26 | export default Mongo; 27 | -------------------------------------------------------------------------------- /templates/js/services/mongo.js: -------------------------------------------------------------------------------- 1 | const mongoose = require('mongoose'); 2 | 3 | class Mongo { 4 | constructor() { 5 | this.url = `mongodb://${process.env.MONGO_HOST}:27017/${process.env.MONGO_SCHEMA}`; 6 | this.options = { 7 | useNewUrlParser: true, 8 | useUnifiedTopology: true 9 | }; 10 | 11 | mongoose.connection.on('connected', () => console.info(`[mongo] connected at ${process.env.MONGO_HOST}/${process.env.MONGO_SCHEMA}`)); 12 | mongoose.connection.on('error', () => console.error('[mongo] error')); 13 | mongoose.connection.on('disconnected', () => console.warn('[mongo] disconnected')); 14 | mongoose.connection.on('reconnectFailed', () => console.error('[mongo] failed')); 15 | } 16 | 17 | async connect() { 18 | await mongoose.connect(this.url, this.options); 19 | } 20 | 21 | async disconnect() { 22 | await mongoose.disconnect(); 23 | } 24 | } 25 | 26 | module.exports = Mongo; 27 | -------------------------------------------------------------------------------- /templates/ts/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-poundcake", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.ts", 6 | "scripts": { 7 | "start": "ts-node src/index.ts", 8 | "start:dev": "nodemon src/index.ts", 9 | "start:prod": "npm run build && node dist/index.js", 10 | "build": "rimraf dist && tsc", 11 | "lint": "eslint . --ext .ts", 12 | "lint-fix": "eslint . --ext .ts --fix", 13 | "test": "echo \"Error: no test specified\" && exit 1" 14 | }, 15 | "keywords": ["typescript", "nodejs", "javascript", "api"], 16 | "author": "Thiago Moraes", 17 | "license": "ISC", 18 | "dependencies": { 19 | "express": "^4.17.1", 20 | "typescript": "^4.0.3", 21 | "dotenv": "^8.2.0" 22 | }, 23 | "devDependencies": { 24 | "@types/node": "^14.11.8", 25 | "@types/express": "^4.17.8", 26 | "@typescript-eslint/eslint-plugin": "^4.4.1", 27 | "@typescript-eslint/parser": "^4.4.1", 28 | "eslint": "^7.11.0", 29 | "nodemon": "^2.0.4", 30 | "ts-node": "^9.0.0" 31 | } 32 | } 33 | -------------------------------------------------------------------------------- /templates/js/base/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "node-api-boot", 3 | "version": "1.0.0", 4 | "description": "", 5 | "main": "src/index.js", 6 | "scripts": { 7 | "start": "node src/index.js", 8 | "start:dev": "nodemon src/index.js", 9 | "generate": "node src/cli.js", 10 | "lint": "eslint . --ext .js", 11 | "lint:fix": "eslint . --ext .js --fix", 12 | "test": "jest" 13 | }, 14 | "husky": { 15 | "hooks": { 16 | "pre-commit": "lint-staged" 17 | } 18 | }, 19 | "lint-staged": { 20 | "*.js": [ 21 | "npm run lint:fix", 22 | "npm test", 23 | "git add" 24 | ] 25 | }, 26 | "jest": { 27 | "testEnvironment": "node", 28 | "coveragePathIgnorePatterns": [ 29 | "/node_modules/" 30 | ] 31 | }, 32 | "keywords": [], 33 | "author": "Thiago Moraes", 34 | "license": "ISC", 35 | "dependencies": { 36 | "dotenv": "^8.2.0" 37 | }, 38 | "devDependencies": { 39 | "eslint": "^7.6.0", 40 | "husky": "^4.2.5", 41 | "jest": "^26.4.0", 42 | "lint-staged": "^10.2.11", 43 | "supertest": "^4.0.2", 44 | "nodemon": "^2.0.4" 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Thiago Moraes 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /templates/ts/services/rabbitmq.ts: -------------------------------------------------------------------------------- 1 | import * as AMQP from 'amqplib'; 2 | 3 | class RabbitMq { 4 | connection: AMQP.Connection 5 | channels: Record = {} 6 | 7 | async connect(): Promise { 8 | this.connection = await AMQP.connect(`amqp://${process.env.RABBITMQ_HOST}`); 9 | this.connection.on('error', (e) => { 10 | console.error('rabbitmq connection error', e); 11 | process.exit(1); 12 | }); 13 | 14 | console.info('rabbitmq connected'); 15 | } 16 | 17 | createChannel(): Promise { 18 | return this.connection.createChannel(); 19 | } 20 | 21 | async assertQueue(queue: string, concurrency = 1, config: AMQP.Options.AssertQueue = { durable: true }): Promise { 22 | console.info('assert queue', queue); 23 | 24 | this.channels[queue] = await this.createChannel(); 25 | this.channels[queue].prefetch(concurrency); 26 | 27 | await this.channels[queue].assertQueue(queue, config); 28 | 29 | this.channels[queue].on('close', () => { 30 | console.error('channel closed'); 31 | process.exit(1); 32 | }); 33 | 34 | this.channels[queue].on('error', e => { 35 | console.error('channel error', e); 36 | process.exit(1); 37 | }); 38 | } 39 | 40 | consumer(queue: string, callback: { (msg: AMQP.Message): Promise | void }, options: AMQP.Options.Consume = {}): void { 41 | if (!this.channels[queue]) { 42 | throw new Error(`channel not exists - ${queue}`); 43 | } 44 | 45 | console.info('consumer', queue); 46 | 47 | this.channels[queue].consume(queue, callback, options); 48 | } 49 | 50 | sendMessage(queue: string, message): void { 51 | this.channels[queue].sendToQueue(queue, Buffer.from(message), { persistent: true }); 52 | } 53 | 54 | ack(queue: string, message: AMQP.Message): void { 55 | this.channels[queue].ack(message); 56 | } 57 | 58 | nack(queue: string, message: AMQP.Message): void { 59 | this.channels[queue].nack(message); 60 | } 61 | } 62 | 63 | export default RabbitMq; 64 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | 2 | # What's Poundcake? 3 | 4 | A NodeJS application generator to scaffold new projects. 5 | 6 | # Installation 7 | 8 | ```bash 9 | npm i -g node-poundcake 10 | ``` 11 | 12 | # Usage 13 | 14 | To generate a default app: 15 | 16 | ```bash 17 | node-poundcake -n 18 | ``` 19 | 20 | ``` 21 | 📦 22 | ┣ 📂__tests__ 23 | ┣ 📂src 24 | ┃ ┗ 📜index.js 25 | ┣ 📜.dockerignore 26 | ┣ 📜.editorconfig 27 | ┣ 📜.eslintrc 28 | ┣ 📜Dockerfile 29 | ┣ 📜README.md 30 | ┗ 📜package.json 31 | ``` 32 | 33 | To generate a **API**: 34 | 35 | ```bash 36 | node-poundcake -n --api 37 | ``` 38 | 39 | ``` 40 | 📦 41 | ┣ 📂__tests__ 42 | ┣ 📂src 43 | ┃ ┣ 📂app 44 | ┃ ┃ ┣ 📜router.js 45 | ┃ ┃ ┗ 📜server.js 46 | ┃ ┣ 📂controllers 47 | ┃ ┃ ┣ 📜index.js 48 | ┃ ┃ ┗ 📜main-controller.js 49 | ┃ ┗ 📜index.js 50 | ┣ 📜.dockerignore 51 | ┣ 📜.editorconfig 52 | ┣ 📜.eslintrc 53 | ┣ 📜Dockerfile 54 | ┣ 📜README.md 55 | ┗ 📜package.json 56 | ``` 57 | 58 | To generate a **Typescript** project (all options have TS support): 59 | 60 | ```bash 61 | node-poundcake -n --ts 62 | ``` 63 | 64 | ``` 65 | 📦 66 | ┣ 📂__tests__ 67 | ┣ 📂src 68 | ┃ ┣ 📂app 69 | ┃ ┃ ┣ 📜router.ts 70 | ┃ ┃ ┗ 📜server.ts 71 | ┃ ┣ 📂controllers 72 | ┃ ┃ ┣ 📜index.ts 73 | ┃ ┃ ┗ 📜main-controller.ts 74 | ┃ ┗ 📜index.ts 75 | ┣ 📜.dockerignore 76 | ┣ 📜.editorconfig 77 | ┣ 📜.eslintrc 78 | ┣ 📜Dockerfile 79 | ┣ 📜package.json 80 | ┗ 📜tsconfig.json 81 | ``` 82 | 83 | To generate a API with **Mongo** and **RabbitMQ**: 84 | 85 | ```bash 86 | node-poundcake -n --api --mongo --rabbitmq 87 | ``` 88 | 89 | ``` 90 | 📦 91 | ┣ 📂__tests__ 92 | ┣ 📂src 93 | ┃ ┣ 📂app 94 | ┃ ┃ ┣ 📜router.js 95 | ┃ ┃ ┗ 📜server.js 96 | ┃ ┣ 📂controllers 97 | ┃ ┃ ┣ 📜index.js 98 | ┃ ┃ ┗ 📜main-controller.js 99 | ┃ ┣ 📂models 100 | ┃ ┃ ┗ 📜contact.js 101 | ┃ ┣ 📂services 102 | ┃ ┃ ┣ 📜index.js 103 | ┃ ┃ ┣ 📜mongo.js 104 | ┃ ┃ ┗ 📜rabbitmq.js 105 | ┃ ┣ 📂subscribers 106 | ┃ ┗ 📜index.js 107 | ┣ 📜.dockerignore 108 | ┣ 📜.editorconfig 109 | ┣ 📜.eslintrc 110 | ┣ 📜Dockerfile 111 | ┣ 📜README.md 112 | ┗ 📜package.json 113 | ``` 114 | 115 | All dependencies will be added on `package.json` according to the options. 116 | 117 | # Command Line Options 118 | 119 | ```bash 120 | # node-poundcake --help 121 | -n, --name project directory name 122 | --ts typescript mode 123 | --api add express api 124 | --mongo add mongoose service 125 | --rabbitmq add amqplib service 126 | --swagger add swagger doc 127 | -f, --force remove directory if exists 128 | -h, --help display help for command 129 | ``` 130 | 131 | # License 132 | 133 | [MIT](LICENSE) 134 | 135 | # E.V.H. 136 | 137 | The name of the application is a tribute to EVH (1955 - 2020). Thanks for everything Eddie. 138 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true, 10 | "modules": true 11 | }, 12 | "ecmaVersion": 2017, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "array-bracket-spacing": [ 17 | 2, 18 | "never" 19 | ], 20 | "block-scoped-var": 2, 21 | "brace-style": [ 22 | 2, 23 | "1tbs" 24 | ], 25 | "camelcase": 0, 26 | "computed-property-spacing": [ 27 | 2, 28 | "never" 29 | ], 30 | "curly": 2, 31 | "eol-last": 2, 32 | "eqeqeq": [ 33 | 2, 34 | "smart" 35 | ], 36 | "max-depth": [ 37 | 1, 38 | 6 39 | ], 40 | "max-len": [ 41 | 1, 42 | 500 43 | ], 44 | "max-statements": [ 45 | 1, 46 | 200 47 | ], 48 | "no-undef": "error", 49 | "no-extend-native": 2, 50 | "no-mixed-spaces-and-tabs": 2, 51 | "no-trailing-spaces": "error", 52 | "no-unused-vars": "error", 53 | "no-use-before-define": [ 54 | 2, 55 | "nofunc" 56 | ], 57 | "object-curly-spacing": [ 58 | 2, 59 | "always" 60 | ], 61 | "quotes": [ 62 | 2, 63 | "single", 64 | "avoid-escape" 65 | ], 66 | "semi": [ 67 | 2, 68 | "always" 69 | ], 70 | "keyword-spacing": [ 71 | 2, 72 | { 73 | "before": true, 74 | "after": true 75 | } 76 | ], 77 | "space-unary-ops": 2, 78 | "comma-spacing": [ 79 | "error", 80 | { 81 | "before": false, 82 | "after": true 83 | } 84 | ], 85 | "space-infix-ops": "error", 86 | "space-before-blocks": [ 87 | "error", 88 | "always" 89 | ], 90 | "indent": [ 91 | "error", 92 | 4, 93 | { 94 | "SwitchCase": 1 95 | } 96 | ], 97 | "no-empty": [ 98 | "error" 99 | ], 100 | "no-multiple-empty-lines": [ 101 | "error", 102 | { 103 | "max": 1, 104 | "maxEOF": 1 105 | } 106 | ], 107 | "padding-line-between-statements": [ 108 | "error", 109 | { 110 | "blankLine": "always", 111 | "prev": "block-like", 112 | "next": "*" 113 | }, 114 | { 115 | "blankLine": "always", 116 | "prev": "*", 117 | "next": "block-like" 118 | } 119 | ] 120 | } 121 | } 122 | -------------------------------------------------------------------------------- /templates/js/base/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "node": true, 4 | "es6": true, 5 | "jest": true 6 | }, 7 | "parserOptions": { 8 | "ecmaFeatures": { 9 | "jsx": true, 10 | "modules": true 11 | }, 12 | "ecmaVersion": 2017, 13 | "sourceType": "module" 14 | }, 15 | "rules": { 16 | "array-bracket-spacing": [ 17 | 2, 18 | "never" 19 | ], 20 | "block-scoped-var": 2, 21 | "brace-style": [ 22 | 2, 23 | "1tbs" 24 | ], 25 | "camelcase": 0, 26 | "computed-property-spacing": [ 27 | 2, 28 | "never" 29 | ], 30 | "curly": 2, 31 | "eol-last": 2, 32 | "eqeqeq": [ 33 | 2, 34 | "smart" 35 | ], 36 | "max-depth": [ 37 | 1, 38 | 6 39 | ], 40 | "max-len": [ 41 | 1, 42 | 500 43 | ], 44 | "max-statements": [ 45 | 1, 46 | 200 47 | ], 48 | "no-undef": "error", 49 | "new-cap": 1, 50 | "no-extend-native": 2, 51 | "no-mixed-spaces-and-tabs": 2, 52 | "no-trailing-spaces": "error", 53 | "no-unused-vars": "error", 54 | "no-use-before-define": [ 55 | 2, 56 | "nofunc" 57 | ], 58 | "object-curly-spacing": [ 59 | 2, 60 | "always" 61 | ], 62 | "quotes": [ 63 | 2, 64 | "single", 65 | "avoid-escape" 66 | ], 67 | "semi": [ 68 | 2, 69 | "always" 70 | ], 71 | "keyword-spacing": [ 72 | 2, 73 | { 74 | "before": true, 75 | "after": true 76 | } 77 | ], 78 | "space-unary-ops": 2, 79 | "comma-spacing": [ 80 | "error", 81 | { 82 | "before": false, 83 | "after": true 84 | } 85 | ], 86 | "space-infix-ops": "error", 87 | "space-before-blocks": [ 88 | "error", 89 | "always" 90 | ], 91 | "indent": [ 92 | "error", 93 | 4, 94 | { 95 | "SwitchCase": 1 96 | } 97 | ], 98 | "no-empty": [ 99 | "error" 100 | ], 101 | "no-multiple-empty-lines": [ 102 | "error", 103 | { 104 | "max": 1, 105 | "maxEOF": 1 106 | } 107 | ], 108 | "padding-line-between-statements": [ 109 | "error", 110 | { 111 | "blankLine": "always", 112 | "prev": "block-like", 113 | "next": "*" 114 | }, 115 | { 116 | "blankLine": "always", 117 | "prev": "*", 118 | "next": "block-like" 119 | } 120 | ] 121 | } 122 | } 123 | -------------------------------------------------------------------------------- /templates/ts/base/.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "parser": "@typescript-eslint/parser", 4 | "plugins": [ 5 | "@typescript-eslint" 6 | ], 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "rules": { 13 | "@typescript-eslint/no-explicit-any": ["error", { "ignoreRestArgs": true }], 14 | "array-bracket-spacing": [ 15 | 2, 16 | "never" 17 | ], 18 | "block-scoped-var": 2, 19 | "brace-style": [ 20 | 2, 21 | "1tbs" 22 | ], 23 | "camelcase": 0, 24 | "computed-property-spacing": [ 25 | 2, 26 | "never" 27 | ], 28 | "curly": 2, 29 | "eol-last": 2, 30 | "eqeqeq": [ 31 | 2, 32 | "smart" 33 | ], 34 | "max-depth": [ 35 | 1, 36 | 6 37 | ], 38 | "max-len": [ 39 | 1, 40 | 500 41 | ], 42 | "max-statements": [ 43 | 1, 44 | 200 45 | ], 46 | "no-extend-native": 2, 47 | "no-mixed-spaces-and-tabs": 2, 48 | "no-trailing-spaces": "error", 49 | "no-unused-vars": "error", 50 | "no-use-before-define": [ 51 | 2, 52 | "nofunc" 53 | ], 54 | "object-curly-spacing": [ 55 | 2, 56 | "always" 57 | ], 58 | "quotes": [ 59 | 2, 60 | "single", 61 | "avoid-escape" 62 | ], 63 | "semi": [ 64 | 2, 65 | "always" 66 | ], 67 | "keyword-spacing": [ 68 | 2, 69 | { 70 | "before": true, 71 | "after": true 72 | } 73 | ], 74 | "space-unary-ops": 2, 75 | "comma-spacing": [ 76 | "error", 77 | { 78 | "before": false, 79 | "after": true 80 | } 81 | ], 82 | "space-infix-ops": "error", 83 | "space-before-blocks": [ 84 | "error", 85 | "always" 86 | ], 87 | "indent": [ 88 | "error", 89 | 4, 90 | { 91 | "SwitchCase": 1 92 | } 93 | ], 94 | "no-empty": [ 95 | "error" 96 | ], 97 | "no-multiple-empty-lines": [ 98 | "error", 99 | { 100 | "max": 1, 101 | "maxEOF": 1 102 | } 103 | ], 104 | "padding-line-between-statements": [ 105 | "error", 106 | { 107 | "blankLine": "always", 108 | "prev": "block-like", 109 | "next": "*" 110 | }, 111 | { 112 | "blankLine": "always", 113 | "prev": "*", 114 | "next": "block-like" 115 | } 116 | ] 117 | } 118 | } 119 | -------------------------------------------------------------------------------- /templates/js/services/rabbitmq.js: -------------------------------------------------------------------------------- 1 | const amqp = require('amqplib'); 2 | 3 | class RabbitMq { 4 | constructor() { 5 | this.connection = false; 6 | this.channels = {}; 7 | } 8 | 9 | async connect() { 10 | try { 11 | this.connection = await amqp.connect(`amqp://${process.env.RABBITMQ_HOST}`); 12 | 13 | this.connection.on('error', (e) => { 14 | console.error('rabbitmq connection error', e); 15 | process.exit(1); 16 | }); 17 | 18 | this.connection.on('blocked', async (e) => { 19 | console.error('rabbitmq connection error', e); 20 | process.exit(1); 21 | }); 22 | 23 | console.info(`[rabbitmq] - connected at port ${process.env.RABBITMQ_HOST}`); 24 | } catch (e) { 25 | throw e; 26 | } 27 | } 28 | 29 | createChannel() { 30 | try { 31 | return this.connection.createChannel(); 32 | } catch (e) { 33 | throw e; 34 | } 35 | } 36 | 37 | getChannel(queue) { 38 | try { 39 | if (!this.channels[queue]) { 40 | throw new Error(`channel not exists - ${queue}`); 41 | } 42 | 43 | return this.channels[queue]; 44 | } catch (e) { 45 | throw e; 46 | } 47 | } 48 | 49 | async assertQueue(queue, concurrency = 1, config = {}) { 50 | try { 51 | console.info('[rabbitmq] - assert queue', queue); 52 | 53 | this.channels[queue] = await this.createChannel(queue); 54 | this.channels[queue].prefetch(concurrency); 55 | 56 | await this.channels[queue].assertQueue(queue, config); 57 | 58 | this.channels[queue].on('close', () => { 59 | console.error('Channel closed'); 60 | process.exit(1); 61 | }); 62 | 63 | this.channels[queue].on('error', e => { 64 | console.error('Channel error', e); 65 | process.exit(1); 66 | }); 67 | } catch (e) { 68 | throw e; 69 | } 70 | } 71 | 72 | consumer(queue, callback, options) { 73 | try { 74 | if (!this.channels[queue]) { 75 | throw new Error(`channel not exists - ${queue}`); 76 | } 77 | 78 | console.info('[rabbitmq] - subscriber', queue); 79 | 80 | this.channels[queue].consume(queue, callback, options); 81 | } catch (e) { 82 | throw e; 83 | } 84 | } 85 | 86 | sendMessage(queue, message) { 87 | try { 88 | return this.channels[queue].sendToQueue(queue, Buffer.from(message)); 89 | } catch (e) { 90 | throw e; 91 | } 92 | } 93 | 94 | ack(queue, message) { 95 | try { 96 | this.channels[queue].ack(message); 97 | } catch (e) { 98 | throw e; 99 | } 100 | } 101 | 102 | nack(queue, message) { 103 | try { 104 | this.channels[queue].nack(message); 105 | } catch (e) { 106 | throw e; 107 | } 108 | } 109 | } 110 | 111 | module.exports = RabbitMq; 112 | -------------------------------------------------------------------------------- /lib/cli.js: -------------------------------------------------------------------------------- 1 | const { program } = require('commander'); 2 | const fs = require('fs'); 3 | const path = require('path'); 4 | 5 | program 6 | .storeOptionsAsProperties(false) 7 | .passCommandToAction(false) 8 | .requiredOption('-n, --name ', 'project directory') 9 | .option('--ts', 'typescript files') 10 | .option('--api', 'add express api') 11 | .option('--mongo', 'add mongoose service') 12 | .option('--rabbitmq', 'add rabbitmq service') 13 | .option('--swagger', 'add swagger doc') 14 | .option('-f, --force', 'remove directory if exists') 15 | .parse(process.argv); 16 | 17 | const options = program.opts(); 18 | const language = options.ts ? 'ts' : 'js'; 19 | const templatesDir = path.join(__dirname, '..', 'templates'); 20 | const baseDir = path.join(__dirname, '..', `templates/${language}`); 21 | const baseFiles = loadFiles(`${baseDir}/base`); 22 | const packageFile = require(`${path.join(__dirname, '..')}/templates/${language}/base/package.json`); 23 | 24 | function loadFiles(path) { 25 | if (!path) { 26 | throw new Error('invalid path'); 27 | } 28 | 29 | const files = {}; 30 | const filenames = fs.readdirSync(path); 31 | 32 | for (const filename of filenames) { 33 | files[filename] = fs.readFileSync(`${path}/${filename}`, 'utf8'); 34 | } 35 | 36 | return files; 37 | } 38 | 39 | function createApp(options) { 40 | try { 41 | if (options.force) { 42 | fs.rmdirSync(options.name, { recursive: true }); 43 | } 44 | 45 | if (fs.existsSync(options.name)) { 46 | throw new Error('directory already exists'); 47 | } 48 | 49 | console.log('[info]', 'build start'); 50 | 51 | packageFile.name = options.name; 52 | 53 | console.log('[info]', 'build base files'); 54 | 55 | buildBaseFiles(baseDir, options.name); 56 | 57 | for (const key in baseFiles) { 58 | fs.writeFileSync(`${options.name}/${key}`, baseFiles[key]); 59 | } 60 | 61 | if (options.mongo || options.rabbitmq) { 62 | fs.mkdirSync(`${options.name}/src/services`); 63 | fs.writeFileSync(`${options.name}/src/services/index.${language}`, buildServicesIndex(options)); 64 | } 65 | 66 | if (options.api) { 67 | console.log('[info]', 'build api files'); 68 | 69 | if (options.ts) { 70 | packageFile.devDependencies['@types/express'] = '^4.17.8'; 71 | } 72 | 73 | packageFile.dependencies.express = '^4.17.1'; 74 | buildApiFiles(baseDir, options.name); 75 | } 76 | 77 | if (options.mongo) { 78 | console.log('[info]', 'build mongo files'); 79 | 80 | if (options.ts) { 81 | packageFile.devDependencies['@types/mongoose'] = '^5.7.36'; 82 | } 83 | 84 | packageFile.dependencies.mongoose = '^5.10.10'; 85 | buildMongoFiles(baseDir, options.name); 86 | } 87 | 88 | if (options.rabbitmq) { 89 | console.log('[info]', 'build rabbitmq files'); 90 | 91 | if (options.ts) { 92 | packageFile.devDependencies['@types/amqplib'] = '^0.5.14'; 93 | } 94 | 95 | packageFile.dependencies.amqplib = '^0.6.0'; 96 | buildRabbitMqFiles(baseDir, options.name); 97 | } 98 | 99 | if (options.swagger) { 100 | packageFile.dependencies['swagger-ui-express'] = '^4.1.4'; 101 | 102 | fs.mkdirSync(`${options.name}/src/config`); 103 | fs.copyFileSync(`${templatesDir}/common/swagger.json`, `${options.name}/src/config/swagger.json`); 104 | } 105 | 106 | console.log('[info]', 'build services files'); 107 | 108 | fs.writeFileSync(`${options.name}/package.json`, JSON.stringify(packageFile, undefined, 4)); 109 | 110 | console.log('[info]', 'app build successfully'); 111 | } catch (error) { 112 | if (error.message !== 'directory already exists') { 113 | fs.rmdirSync(options.name, { recursive: true }); 114 | } 115 | 116 | console.error(error); 117 | } 118 | } 119 | 120 | function buildBaseFiles(baseDir, appname) { 121 | fs.mkdirSync(appname); 122 | fs.mkdirSync(`${appname}/src`); 123 | fs.mkdirSync(`${appname}/__tests__`); 124 | fs.copyFileSync(`${baseDir}/index.${language}`, `${appname}/src/index.${language}`); 125 | } 126 | 127 | function buildApiFiles(baseDir, appname) { 128 | fs.mkdirSync(`${appname}/src/app`); 129 | fs.mkdirSync(`${appname}/src/controllers`); 130 | 131 | fs.copyFileSync(`${baseDir}/app/server.${language}`, `${appname}/src/app/server.${language}`); 132 | fs.copyFileSync(`${baseDir}/app/router.${language}`, `${appname}/src/app/router.${language}`); 133 | 134 | fs.copyFileSync(`${baseDir}/controllers/index.${language}`, `${appname}/src/controllers/index.${language}`); 135 | fs.copyFileSync(`${baseDir}/controllers/main-controller.${language}`, `${appname}/src/controllers/main-controller.${language}`); 136 | } 137 | 138 | function buildMongoFiles(baseDir, appname) { 139 | fs.mkdirSync(`${appname}/src/models`); 140 | fs.copyFileSync(`${baseDir}/models/contact.${language}`, `${appname}/src/models/contact.${language}`); 141 | fs.copyFileSync(`${baseDir}/services/mongo.${language}`, `${appname}/src/services/mongo.${language}`); 142 | } 143 | 144 | function buildRabbitMqFiles(baseDir, appname) { 145 | fs.mkdirSync(`${appname}/src/subscribers`); 146 | fs.copyFileSync(`${baseDir}/services/rabbitmq.${language}`, `${appname}/src/services/rabbitmq.${language}`); 147 | } 148 | 149 | function buildServicesIndex(options) { 150 | let imports = ''; 151 | let instances = ''; 152 | 153 | if (options.mongo) { 154 | imports += 'const MongoService = require(\'./mongo-service\');\n'; 155 | instances += 'exports.mongoService = new MongoService();\n'; 156 | } 157 | 158 | if (options.rabbitmq) { 159 | imports += 'const RabbitMqService = require(\'./rabbitmq-service\');\n'; 160 | instances += 'exports.rabbitMqService = new RabbitMqService();\n'; 161 | } 162 | 163 | return `${imports}\n${instances}`; 164 | } 165 | 166 | createApp(options); 167 | -------------------------------------------------------------------------------- /templates/common/swagger.json: -------------------------------------------------------------------------------- 1 | { 2 | "openapi": "3.0.0", 3 | "info": { 4 | "description": "", 5 | "version": "1.0.0", 6 | "title": "node-poundcake" 7 | }, 8 | "servers": [ 9 | { 10 | "url": "http://localhost", 11 | "description": "" 12 | } 13 | ], 14 | "security": [ 15 | { 16 | "bearerAuth": [] 17 | } 18 | ], 19 | "paths": { 20 | "/user": { 21 | "post": { 22 | "tags": [ 23 | "User" 24 | ], 25 | "summary": "Post User", 26 | "description": "", 27 | "requestBody": { 28 | "content": { 29 | "application/json": { 30 | "schema": { 31 | "type": "object" 32 | }, 33 | "examples": { 34 | "request": { 35 | "value": { 36 | "email": "", 37 | "password": "" 38 | } 39 | } 40 | } 41 | } 42 | } 43 | }, 44 | "responses": { 45 | "200": { 46 | "description": "Success", 47 | "content": { 48 | "application/json": { 49 | "schema": { 50 | "type": "object" 51 | }, 52 | "examples": { 53 | "response": { 54 | "value": { 55 | "id": 1, 56 | "email": "", 57 | "password": "" 58 | } 59 | } 60 | } 61 | } 62 | } 63 | }, 64 | "401": { 65 | "description": "Unauthorized", 66 | "content": { 67 | "application/json": { 68 | "schema": { 69 | "type": "object" 70 | }, 71 | "examples": { 72 | "response": { 73 | "value": { 74 | "message": "Unauthorized" 75 | } 76 | } 77 | } 78 | } 79 | } 80 | }, 81 | "500": { 82 | "description": "Server error", 83 | "content": { 84 | "application/json": { 85 | "schema": { 86 | "type": "object" 87 | }, 88 | "examples": { 89 | "response": { 90 | "value": { 91 | "message": "Server error" 92 | } 93 | } 94 | } 95 | } 96 | } 97 | } 98 | } 99 | }, 100 | "get": { 101 | "tags": [ 102 | "User" 103 | ], 104 | "summary": "Get Users", 105 | "description": "", 106 | "responses": { 107 | "200": { 108 | "description": "Success", 109 | "content": { 110 | "application/json": { 111 | "schema": { 112 | "type": "object" 113 | }, 114 | "examples": { 115 | "response": { 116 | "value": [{ 117 | "email": "", 118 | "password": "" 119 | }] 120 | } 121 | } 122 | } 123 | } 124 | }, 125 | "401": { 126 | "description": "Unauthorized", 127 | "content": { 128 | "application/json": { 129 | "schema": { 130 | "type": "object" 131 | }, 132 | "examples": { 133 | "response": { 134 | "value": { 135 | "message": "Unauthorized" 136 | } 137 | } 138 | } 139 | } 140 | } 141 | }, 142 | "500": { 143 | "description": "Server error", 144 | "content": { 145 | "application/json": { 146 | "schema": { 147 | "type": "object" 148 | }, 149 | "examples": { 150 | "response": { 151 | "value": { 152 | "message": "Server error" 153 | } 154 | } 155 | } 156 | } 157 | } 158 | } 159 | } 160 | } 161 | }, 162 | "/user/{userId}": { 163 | "get": { 164 | "tags": [ 165 | "User" 166 | ], 167 | "summary": "Get User", 168 | "description": "", 169 | "parameters": [ 170 | { 171 | "in": "path", 172 | "name": "userId", 173 | "required": true, 174 | "schema": { 175 | "type": "string" 176 | } 177 | } 178 | ], 179 | "responses": { 180 | "200": { 181 | "description": "Success", 182 | "content": { 183 | "application/json": { 184 | "schema": { 185 | "type": "object" 186 | }, 187 | "examples": { 188 | "response": { 189 | "value": { 190 | "email": "", 191 | "password": "" 192 | } 193 | } 194 | } 195 | } 196 | } 197 | }, 198 | "401": { 199 | "description": "Unauthorized", 200 | "content": { 201 | "application/json": { 202 | "schema": { 203 | "type": "object" 204 | }, 205 | "examples": { 206 | "response": { 207 | "value": { 208 | "message": "Unauthorized" 209 | } 210 | } 211 | } 212 | } 213 | } 214 | }, 215 | "500": { 216 | "description": "Server error", 217 | "content": { 218 | "application/json": { 219 | "schema": { 220 | "type": "object" 221 | }, 222 | "examples": { 223 | "response": { 224 | "value": { 225 | "message": "Server error" 226 | } 227 | } 228 | } 229 | } 230 | } 231 | } 232 | } 233 | }, 234 | "put": { 235 | "tags": [ 236 | "User" 237 | ], 238 | "requestBody": { 239 | "content": { 240 | "application/json": { 241 | "schema": { 242 | "type": "object" 243 | }, 244 | "examples": { 245 | "request": { 246 | "value": { 247 | "email": "", 248 | "password": "" 249 | } 250 | } 251 | } 252 | } 253 | } 254 | }, 255 | "summary": "Update User", 256 | "description": "", 257 | "parameters": [ 258 | { 259 | "in": "path", 260 | "name": "userId", 261 | "required": true, 262 | "schema": { 263 | "type": "string" 264 | } 265 | } 266 | ], 267 | "responses": { 268 | "200": { 269 | "description": "Success", 270 | "content": { 271 | "application/json": { 272 | "schema": { 273 | "type": "object" 274 | }, 275 | "examples": { 276 | "response": { 277 | "value": { 278 | "email": "", 279 | "password": "" 280 | } 281 | } 282 | } 283 | } 284 | } 285 | }, 286 | "401": { 287 | "description": "Unauthorized", 288 | "content": { 289 | "application/json": { 290 | "schema": { 291 | "type": "object" 292 | }, 293 | "examples": { 294 | "response": { 295 | "value": { 296 | "message": "Unauthorized" 297 | } 298 | } 299 | } 300 | } 301 | } 302 | }, 303 | "500": { 304 | "description": "Server error", 305 | "content": { 306 | "application/json": { 307 | "schema": { 308 | "type": "object" 309 | }, 310 | "examples": { 311 | "response": { 312 | "value": { 313 | "message": "Server error" 314 | } 315 | } 316 | } 317 | } 318 | } 319 | } 320 | } 321 | } 322 | } 323 | }, 324 | "components": { 325 | "securitySchemes": { 326 | "bearerAuth": { 327 | "type": "http", 328 | "scheme": "bearer", 329 | "bearerFormat": "JWT" 330 | } 331 | } 332 | } 333 | } 334 | --------------------------------------------------------------------------------