├── .gitignore ├── LICENSE ├── README.md ├── packages ├── hello │ ├── README.md │ ├── index.js │ └── package.json ├── intro │ └── README.md ├── real-world │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── packages │ │ ├── backend │ │ │ ├── .env.default │ │ │ ├── .gitignore │ │ │ ├── constants │ │ │ │ └── index.js │ │ │ ├── core │ │ │ │ └── README.md │ │ │ ├── mixins │ │ │ │ ├── __tests__ │ │ │ │ │ └── knexdb.mixin.spec.js │ │ │ │ └── knexdb.mixin.js │ │ │ ├── models │ │ │ │ └── user.model.js │ │ │ ├── moleculer.config.js │ │ │ ├── package.json │ │ │ ├── services │ │ │ │ ├── articles │ │ │ │ │ └── README.md │ │ │ │ ├── auth │ │ │ │ │ └── README.md │ │ │ │ ├── databases │ │ │ │ │ └── usr │ │ │ │ │ │ └── user.service.js │ │ │ │ ├── gateway │ │ │ │ │ └── gateway.service.js │ │ │ │ ├── interaction │ │ │ │ │ └── README.md │ │ │ │ └── users │ │ │ │ │ ├── README.md │ │ │ │ │ └── user.service.js │ │ │ └── utils │ │ │ │ ├── __tests__ │ │ │ │ └── db.util.spec.js │ │ │ │ └── db.util.js │ │ └── database │ │ │ ├── .env.default │ │ │ ├── .gitignore │ │ │ ├── helpers │ │ │ └── index.js │ │ │ ├── knexfile.js │ │ │ ├── migrations │ │ │ ├── 20200411002210_create_user_schema.js │ │ │ └── 20200411002300_create_user_table.js │ │ │ └── package.json │ └── yarn.lock ├── todo-app │ ├── docker-compose.yml │ ├── package.json │ ├── packages │ │ ├── backend │ │ │ ├── .gitignore │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── package.json │ │ │ └── src │ │ │ │ ├── app.js │ │ │ │ ├── controllers │ │ │ │ └── TodoController.js │ │ │ │ ├── models │ │ │ │ ├── tests │ │ │ │ │ └── todo.spec.js │ │ │ │ └── todo.js │ │ │ │ └── router │ │ │ │ └── index.js │ │ └── frontend │ │ │ ├── .browserslistrc │ │ │ ├── .dockerignore │ │ │ ├── .env.production │ │ │ ├── .gitignore │ │ │ ├── .prettierrc │ │ │ ├── Dockerfile │ │ │ ├── README.md │ │ │ ├── cypress.json │ │ │ ├── jest.config.js │ │ │ ├── nginx.conf │ │ │ ├── package.json │ │ │ ├── public │ │ │ ├── favicon.ico │ │ │ └── index.html │ │ │ ├── src │ │ │ ├── App.vue │ │ │ ├── assets │ │ │ │ ├── logo.png │ │ │ │ └── style.css │ │ │ ├── components │ │ │ │ └── AppFooter.vue │ │ │ ├── main.ts │ │ │ ├── router │ │ │ │ └── index.ts │ │ │ ├── service │ │ │ │ └── index.ts │ │ │ ├── shims-tsx.d.ts │ │ │ ├── shims-vue.d.ts │ │ │ ├── store │ │ │ │ └── index.ts │ │ │ └── views │ │ │ │ └── Home │ │ │ │ ├── HomeService.ts │ │ │ │ └── index.vue │ │ │ ├── tests │ │ │ ├── e2e │ │ │ │ ├── plugins │ │ │ │ │ └── index.js │ │ │ │ ├── specs │ │ │ │ │ └── test.js │ │ │ │ └── support │ │ │ │ │ ├── commands.js │ │ │ │ │ └── index.js │ │ │ └── unit │ │ │ │ └── example.spec.ts │ │ │ ├── tsconfig.json │ │ │ ├── tslint.json │ │ │ ├── vue.config.js │ │ │ └── yarn.lock │ └── yarn.lock └── web │ └── README.md └── provision ├── .gitignore ├── clean.sh ├── docker-compose.yml ├── get-docker.sh ├── images └── node-builder-10-alpine │ └── Dockerfile ├── setup-docker.sh └── setup.sh /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript v1 declaration files 45 | typings/ 46 | 47 | # TypeScript cache 48 | *.tsbuildinfo 49 | 50 | # Optional npm cache directory 51 | .npm 52 | 53 | # Optional eslint cache 54 | .eslintcache 55 | 56 | # Microbundle cache 57 | .rpt2_cache/ 58 | .rts2_cache_cjs/ 59 | .rts2_cache_es/ 60 | .rts2_cache_umd/ 61 | 62 | # Optional REPL history 63 | .node_repl_history 64 | 65 | # Output of 'npm pack' 66 | *.tgz 67 | 68 | # Yarn Integrity file 69 | .yarn-integrity 70 | 71 | # dotenv environment variables file 72 | .env 73 | .env.test 74 | 75 | # parcel-bundler cache (https://parceljs.org/) 76 | .cache 77 | 78 | # Next.js build output 79 | .next 80 | 81 | # Nuxt.js build / generate output 82 | .nuxt 83 | dist 84 | 85 | # Gatsby files 86 | .cache/ 87 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 88 | # https://nextjs.org/blog/next-9-1#public-directory-support 89 | # public 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # TernJS port file 104 | .tern-port 105 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 LTV Co., Ltd 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # nodejs-course 2 | 3 | LTV NodeJS Course 4 | 5 | > - Period: 1 month (`March 09th, 2020 - April 11th, 2020`) 6 | > - 3 times per week 7 | 8 | ## Section 1: Introduction and Setup 9 | 10 | - Web Mind Set 11 | - Introduction and the Goal of this Course 12 | - How to play together 13 | - npm, yarn, git & some Command Line References 14 | - Install NodeJS 15 | 16 | ## Section 2: Playground 17 | 18 | - Introduce about javascript ES6 19 | - Anonymous function 20 | - How to run nodejs 21 | - HelloWorld 22 | 23 | ## Section 3: How to start new NodeJS web project with ExpressJS Framework 24 | 25 | - Configuration 26 | - Modules, Exports, and Require 27 | - Package Managers 28 | - Init, nodemon, package.json 29 | - Routes 30 | - Static Files and Middleware 31 | - Template & Template Engines 32 | - QueryString & Post Parameters 33 | - RESTful APIs & JSON 34 | - Structuring an APP 35 | 36 | ## Section 4: Javascript, JSON, and Databases 37 | 38 | - MySQL with ORM 39 | - MongoDB & Mongoose 40 | 41 | ## Section 5: Use a Boilerplate to kick-start your nodejs project 42 | 43 | - NodeJS-KickStart 44 | - Authentication with passport 45 | - Development & Production mode 46 | - How to deploy your project 47 | - DevOps 48 | 49 | ## Section 6: Real Project 50 | 51 | - Project Management System. 52 | 53 | ## Section 7: Deployment 54 | 55 | - Docker 56 | - Docker Swarm 57 | - Kubernetes 58 | -------------------------------------------------------------------------------- /packages/hello/README.md: -------------------------------------------------------------------------------- 1 | # Hello World Project 2 | 3 | > This project used to show the basic code of Javascript run on NodeJS platform 4 | > 5 | > **Show you**: 6 | > 7 | > - How to print console: Normally, use to log 8 | > - How to define variable and use variable 9 | > - `package.json` file 10 | > - How to run the NodeJS project 11 | -------------------------------------------------------------------------------- /packages/hello/index.js: -------------------------------------------------------------------------------- 1 | console.log('Hello World'); 2 | 3 | const a = 1; // Never change later 4 | let b = 2; // Change change 5 | 6 | // Trick: Always use `const` 7 | // When you need to change the value then you can modify `const` -> `let` 8 | // 9 | 10 | const c = a + b; 11 | console.log('Result: ', c); 12 | console.log('Result: ' + c); 13 | console.log(`Result should be: ${c}`) 14 | console.warn('Hi All') 15 | console.error('My Error') 16 | -------------------------------------------------------------------------------- /packages/hello/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "hello", 3 | "version": "0.1.0", 4 | "description": "Hello World Project", 5 | "main": "index.js", 6 | "repository": "https://github.com/ltv/ltv-nodejs.git", 7 | "author": "Luc ", 8 | "license": "MIT", 9 | "scripts": { 10 | "start": "node index.js", 11 | "serve": "nodemon index.js" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/intro/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/intro/README.md -------------------------------------------------------------------------------- /packages/real-world/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node 2 | # Edit at https://www.gitignore.io/?templates=node 3 | 4 | ### Node ### 5 | # Logs 6 | logs 7 | *.log 8 | npm-debug.log* 9 | yarn-debug.log* 10 | yarn-error.log* 11 | lerna-debug.log* 12 | 13 | # Runtime data 14 | pids 15 | *.pid 16 | *.seed 17 | *.pid.lock 18 | 19 | # Directory for instrumented libs generated by jscoverage/JSCover 20 | lib-cov 21 | 22 | # Coverage directory used by tools like istanbul 23 | coverage 24 | *.lcov 25 | 26 | # nyc test coverage 27 | .nyc_output 28 | 29 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 30 | .grunt 31 | 32 | # Bower dependency directory (https://bower.io/) 33 | bower_components 34 | 35 | # node-waf configuration 36 | .lock-wscript 37 | 38 | # Compiled binary addons (https://nodejs.org/api/addons.html) 39 | build/Release 40 | 41 | # Dependency directories 42 | node_modules/ 43 | jspm_packages/ 44 | 45 | # TypeScript v1 declaration files 46 | typings/ 47 | 48 | # TypeScript cache 49 | *.tsbuildinfo 50 | 51 | # Optional npm cache directory 52 | .npm 53 | 54 | # Optional eslint cache 55 | .eslintcache 56 | 57 | # Optional REPL history 58 | .node_repl_history 59 | 60 | # Output of 'npm pack' 61 | *.tgz 62 | 63 | # Yarn Integrity file 64 | .yarn-integrity 65 | 66 | # dotenv environment variables file 67 | .env 68 | .env.test 69 | 70 | # parcel-bundler cache (https://parceljs.org/) 71 | .cache 72 | 73 | # next.js build output 74 | .next 75 | 76 | # nuxt.js build output 77 | .nuxt 78 | 79 | # rollup.js default build output 80 | dist/ 81 | 82 | # Uncomment the public line if your project uses Gatsby 83 | # https://nextjs.org/blog/next-9-1#public-directory-support 84 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 85 | # public 86 | 87 | # Storybook build outputs 88 | .out 89 | .storybook-out 90 | 91 | # vuepress build output 92 | .vuepress/dist 93 | 94 | # Serverless directories 95 | .serverless/ 96 | 97 | # FuseBox cache 98 | .fusebox/ 99 | 100 | # DynamoDB Local files 101 | .dynamodb/ 102 | 103 | # Temporary folders 104 | tmp/ 105 | temp/ 106 | 107 | # End of https://www.gitignore.io/api/node 108 | -------------------------------------------------------------------------------- /packages/real-world/README.md: -------------------------------------------------------------------------------- 1 | # RealWorld 2 | 3 | ## Microservice Architecture 4 | 5 | - What is micro-service architecture 6 | - How to communicate between each service 7 | - Should we use microservice architecture 8 | -------------------------------------------------------------------------------- /packages/real-world/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "real-world", 3 | "version": "0.1.0", 4 | "description": "Real World example, a medium clone web application for sharing articles.", 5 | "main": "index.js", 6 | "repository": "https://github.com/ltv/nodejs-course.git", 7 | "author": "Luc ", 8 | "license": "MIT", 9 | "private": true, 10 | "workspaces": { 11 | "packages": [ 12 | "packages/*" 13 | ] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/.env.default: -------------------------------------------------------------------------------- 1 | DATABASE_HOST=localhost 2 | DATABASE_PORT=5432 3 | DATABASE_USER=ltv 4 | DATABASE_PASS=123798 5 | DATABASE_NAME=rw 6 | DATABASE_POOL_MIN=0 7 | DATABASE_POOL_MAX=1 8 | SALT_ROUNDS=10 9 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/.gitignore: -------------------------------------------------------------------------------- 1 | .env.* 2 | !.env.default 3 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/constants/index.js: -------------------------------------------------------------------------------- 1 | exports.SERVICE_USER = 'user'; 2 | exports.TABLE_USER = 'User'; 3 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/core/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/real-world/packages/backend/core/README.md -------------------------------------------------------------------------------- /packages/real-world/packages/backend/mixins/__tests__/knexdb.mixin.spec.js: -------------------------------------------------------------------------------- 1 | const { DbMixin } = require('../knexdb.mixin'); 2 | 3 | describe('DbMixin', () => { 4 | it('Should create mixin with options', () => { 5 | const dbMixin = DbMixin({ table: 'myTable' }); 6 | 7 | expect(dbMixin).toBeTruthy(); 8 | }); 9 | 10 | it('Should throw exception if provide options without `table`', () => { 11 | const createMixin = () => { 12 | DbMixin({ schema: 'usr' }); 13 | }; 14 | 15 | expect(createMixin).toThrowError( 16 | new Error('Table is required but not mentioned in options') 17 | ); 18 | }); 19 | }); 20 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/mixins/knexdb.mixin.js: -------------------------------------------------------------------------------- 1 | const { KnexDbMixin } = require('moleculer-db-knex'); 2 | 3 | const { getKnexConfig } = require('../utils/db.util'); 4 | 5 | exports.DbMixin = (options) => { 6 | const { table, schema, idField } = options || { 7 | schema: 'public', 8 | idField: 'id', 9 | }; 10 | if (!table) { 11 | throw new Error('Table is required but not mentioned in options'); 12 | } 13 | const configs = getKnexConfig(); 14 | return KnexDbMixin({ 15 | schema, 16 | table, 17 | idField, 18 | knex: { 19 | configs, 20 | }, 21 | }); 22 | }; 23 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/models/user.model.js: -------------------------------------------------------------------------------- 1 | module.exports = {}; 2 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/moleculer.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | nodeID: 'ltv-nodejs-course', 3 | logLevel: 'info', 4 | 5 | serializer: 'JSON', 6 | 7 | // cacher: { 8 | // type: 'memory', 9 | // options: { 10 | // ttl: 10, // 30 seconds 11 | // }, 12 | // }, 13 | cacher: 'redis://localhost:6379', 14 | 15 | //requestTimeout: 10 * 1000, 16 | retryPolicy: { 17 | enabled: false, 18 | retries: 5, 19 | delay: 100, 20 | maxDelay: 1000, 21 | factor: 2, 22 | check: (err) => err && !!err.retryable, 23 | }, 24 | 25 | maxCallLevel: 100, 26 | heartbeatInterval: 5, 27 | heartbeatTimeout: 15, 28 | 29 | tracking: { 30 | enabled: false, 31 | shutdownTimeout: 5000, 32 | }, 33 | 34 | disableBalancer: false, 35 | 36 | registry: { 37 | strategy: 'RoundRobin', 38 | preferLocal: true, 39 | }, 40 | 41 | circuitBreaker: { 42 | enabled: false, 43 | threshold: 0.5, 44 | windowTime: 60, 45 | minRequestCount: 20, 46 | halfOpenTime: 10 * 1000, 47 | check: (err) => err && err.code >= 500, 48 | }, 49 | 50 | bulkhead: { 51 | enabled: false, 52 | concurrency: 10, 53 | maxQueueSize: 100, 54 | }, 55 | 56 | validation: true, 57 | 58 | tracing: { 59 | enabled: true, 60 | events: true, 61 | exporter: [ 62 | { 63 | type: 'Console', 64 | options: { 65 | width: 100, 66 | colors: true, 67 | logger: console.log, 68 | }, 69 | }, 70 | ], 71 | }, 72 | 73 | hotReload: true, // update code, auto reload server `nodemon` 74 | }; 75 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@app/backend", 3 | "version": "0.1.0", 4 | "description": "Real World backend", 5 | "main": "index.js", 6 | "author": "Luc ", 7 | "license": "MIT", 8 | "private": true, 9 | "scripts": { 10 | "serve": "moleculer-runner --env --repl --hot services --mask **/*.service.js --envfile=.env.development", 11 | "test:unit": "jest --coverage" 12 | }, 13 | "dependencies": { 14 | "body-parser": "^1.19.0", 15 | "dotenv": "^8.2.0", 16 | "ioredis": "^4.16.2", 17 | "knex": "^0.20.13", 18 | "moleculer": "^0.14.5", 19 | "moleculer-db-knex": "^0.1.13", 20 | "moleculer-web": "^0.9.1", 21 | "pg": "^8.0.2", 22 | "redlock": "^4.1.0" 23 | }, 24 | "devDependencies": { 25 | "jest": "^25.3.0", 26 | "moleculer-repl": "^0.6.3" 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/services/articles/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/real-world/packages/backend/services/articles/README.md -------------------------------------------------------------------------------- /packages/real-world/packages/backend/services/auth/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/real-world/packages/backend/services/auth/README.md -------------------------------------------------------------------------------- /packages/real-world/packages/backend/services/databases/usr/user.service.js: -------------------------------------------------------------------------------- 1 | const { TABLE_USER } = require('../../../constants'); 2 | const { DbMixin } = require('../../../mixins/knexdb.mixin'); 3 | 4 | module.exports = { 5 | name: `db-${TABLE_USER}`, 6 | mixins: [DbMixin({ schema: 'usr', table: 'User', idField: 'usrId' })], 7 | }; 8 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/services/gateway/gateway.service.js: -------------------------------------------------------------------------------- 1 | const ApiGateway = require('moleculer-web'); 2 | const { SERVICE_USER } = require('../../constants'); 3 | 4 | module.exports = { 5 | mixins: [ApiGateway], 6 | settings: { 7 | routes: [ 8 | { 9 | port: 3000, 10 | path: '/api', 11 | authentication: false, 12 | autoAliases: false, 13 | aliases: { 14 | 'GET /users': `${SERVICE_USER}.getAllUsers`, 15 | }, 16 | // Use bodyparser modules 17 | bodyParsers: { 18 | json: { limit: '2MB' }, 19 | urlencoded: { extended: true, limit: '2MB' }, 20 | }, 21 | }, 22 | ], 23 | }, 24 | }; 25 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/services/interaction/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/real-world/packages/backend/services/interaction/README.md -------------------------------------------------------------------------------- /packages/real-world/packages/backend/services/users/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/real-world/packages/backend/services/users/README.md -------------------------------------------------------------------------------- /packages/real-world/packages/backend/services/users/user.service.js: -------------------------------------------------------------------------------- 1 | const { SERVICE_USER, TABLE_USER } = require('../../constants'); 2 | 3 | module.exports = { 4 | name: SERVICE_USER, 5 | mixins: [], 6 | actions: { 7 | getAllUsers: { 8 | cache: { 9 | keys: ['active'], 10 | ttl: 10, 11 | }, 12 | params: { 13 | active: { 14 | type: 'string', 15 | optional: true, 16 | }, 17 | }, 18 | handler(ctx) { 19 | const { active } = ctx.params; 20 | return ctx.call(`db-${TABLE_USER}.find`, { 21 | where: { actFlg: active == 'true' }, 22 | }); 23 | }, 24 | }, 25 | }, 26 | }; 27 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/utils/__tests__/db.util.spec.js: -------------------------------------------------------------------------------- 1 | const { getKnexConfig } = require('../db.util'); 2 | describe('db.util', () => { 3 | it('should return correct config', () => { 4 | const configs = getKnexConfig(); 5 | expect(configs).toEqual({ 6 | client: 'postgresql', 7 | connection: expect.any(Object), 8 | pool: { 9 | min: expect.any(Number), 10 | max: expect.any(Number), 11 | }, 12 | }); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /packages/real-world/packages/backend/utils/db.util.js: -------------------------------------------------------------------------------- 1 | const getDbConnections = () => { 2 | const { 3 | DATABASE_HOST, 4 | DATABASE_PORT, 5 | DATABASE_USER, 6 | DATABASE_PASS, 7 | DATABASE_NAME, 8 | } = process.env; 9 | 10 | return { 11 | host: DATABASE_HOST, 12 | port: +DATABASE_PORT, 13 | database: DATABASE_NAME, 14 | user: DATABASE_USER, 15 | password: DATABASE_PASS, 16 | }; 17 | }; 18 | 19 | exports.getKnexConfig = () => { 20 | const connection = getDbConnections(); 21 | const { DATABASE_POOL_MIN: min, DATABASE_POOL_MAX: max } = process.env; 22 | 23 | return { 24 | client: 'postgresql', 25 | connection, 26 | pool: { 27 | min: +min, 28 | max: +max, 29 | }, 30 | }; 31 | }; 32 | -------------------------------------------------------------------------------- /packages/real-world/packages/database/.env.default: -------------------------------------------------------------------------------- 1 | DATABASE_HOST=localhost 2 | DATABASE_PORT=5432 3 | DATABASE_USER=ltv 4 | DATABASE_PASS=123798 5 | DATABASE_NAME=rw 6 | DATABASE_POOL_MIN=0 7 | DATABASE_POOL_MAX=1 8 | SALT_ROUNDS=10 9 | -------------------------------------------------------------------------------- /packages/real-world/packages/database/.gitignore: -------------------------------------------------------------------------------- 1 | .env.* 2 | !.env.default 3 | -------------------------------------------------------------------------------- /packages/real-world/packages/database/helpers/index.js: -------------------------------------------------------------------------------- 1 | const DEFAULT_MUTATION_SET_OPTS = { 2 | creUsrId: true, 3 | updUsrId: true, 4 | creDt: true, 5 | updDt: true, 6 | }; 7 | 8 | const DEFAULT_BASE_FLAG_OPTS = { 9 | actFlg: true, 10 | delFlg: true, 11 | }; 12 | 13 | exports.mutationSet = ( 14 | knex, 15 | tb, 16 | options = { 17 | creUsrId: true, 18 | updUsrId: true, 19 | creDt: true, 20 | updDt: true, 21 | } 22 | ) => { 23 | const { creUsrId, updUsrId, creDt, updDt } = { 24 | ...DEFAULT_MUTATION_SET_OPTS, 25 | ...options, 26 | }; 27 | if (creUsrId) tb.integer('creUsrId').comment('Create User Id'); 28 | if (updUsrId) tb.integer('updUsrId').comment('Create User Id'); 29 | if (creDt) 30 | tb.timestamp('creDt').defaultTo(knex.fn.now()).comment('Create Date'); 31 | if (updDt) 32 | tb.timestamp('updDt').defaultTo(knex.fn.now()).comment('Update Date'); 33 | return tb; 34 | }; 35 | 36 | exports.baseFlag = (tb, options = { actFlg: true, delFlg: true }) => { 37 | const { actFlg, delFlg } = { ...DEFAULT_BASE_FLAG_OPTS, ...options }; 38 | if (actFlg) tb.boolean('actFlg').defaultTo(true).comment('Active Flag'); 39 | if (delFlg) tb.boolean('delFlg').defaultTo(false).comment('Delete Flag'); 40 | return tb; 41 | }; 42 | 43 | exports.up = function (knex, Promise) {}; 44 | exports.down = function (knex, Promise) {}; 45 | -------------------------------------------------------------------------------- /packages/real-world/packages/database/knexfile.js: -------------------------------------------------------------------------------- 1 | const path = require('path'); 2 | 3 | const NODE_ENV = process.env.NODE_ENV || 'development'; 4 | require('dotenv').config({ path: `.env.${NODE_ENV}` }); 5 | 6 | const MIGRATIONS_DIR = path.resolve(__dirname, 'migrations'); 7 | const SEEDS_DIR = path.resolve(__dirname, 'seeds', NODE_ENV); 8 | 9 | const baseConfigs = { 10 | client: 'postgresql', 11 | connection: { 12 | host: process.env.DATABASE_HOST, 13 | port: process.env.DATABASE_PORT, 14 | database: process.env.DATABASE_NAME, 15 | user: process.env.DATABASE_USER, 16 | password: process.env.DATABASE_PASS, 17 | ssl: process.env.DATABASE_SSL, 18 | }, 19 | pool: { 20 | min: +process.env.DATABASE_POOL_MIN, 21 | max: +process.env.DATABASE_POOL_MAX, 22 | }, 23 | migrations: { 24 | directory: MIGRATIONS_DIR, 25 | tableName: 'knex_migrations', 26 | schemaName: 'public', 27 | }, 28 | seeds: { 29 | directory: SEEDS_DIR, 30 | }, 31 | }; 32 | 33 | module.exports = { 34 | development: { 35 | ...baseConfigs, 36 | }, 37 | 38 | staging: { 39 | ...baseConfigs, 40 | }, 41 | 42 | test: { 43 | ...baseConfigs, 44 | }, 45 | 46 | production: { 47 | ...baseConfigs, 48 | }, 49 | }; 50 | -------------------------------------------------------------------------------- /packages/real-world/packages/database/migrations/20200411002210_create_user_schema.js: -------------------------------------------------------------------------------- 1 | exports.up = function (knex, Promise) { 2 | return knex.schema.raw('CREATE SCHEMA IF NOT EXISTS usr;'); // Profile Module 3 | }; 4 | 5 | exports.down = function (knex, Promise) { 6 | return knex.schema; 7 | }; 8 | -------------------------------------------------------------------------------- /packages/real-world/packages/database/migrations/20200411002300_create_user_table.js: -------------------------------------------------------------------------------- 1 | const { mutationSet, baseFlag } = require('../helpers'); 2 | 3 | exports.up = function (knex, Promise) { 4 | return knex.schema.withSchema('usr').createTable('User', function (tb) { 5 | tb.increments('usrId').comment('User Id'); 6 | tb.string('usrNm').unique().comment('Username'); 7 | tb.string('usrEml').unique().comment('User Email'); 8 | tb.string('usrPwd').comment('User Password'); 9 | 10 | baseFlag(tb); 11 | mutationSet(knex, tb); 12 | 13 | tb.comment('User'); 14 | }); 15 | }; 16 | 17 | exports.down = function (knex, Promise) { 18 | return knex.schema; 19 | }; 20 | -------------------------------------------------------------------------------- /packages/real-world/packages/database/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@app/database", 3 | "version": "0.1.0", 4 | "description": "Database migration", 5 | "main": "index.js", 6 | "author": "Luc ", 7 | "license": "MIT", 8 | "scripts": { 9 | "migrate": "export NODE_ENV=development && knex migrate:latest" 10 | }, 11 | "dependencies": { 12 | "dotenv": "^8.2.0", 13 | "knex": "^0.20.13", 14 | "pg": "^8.0.2" 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /packages/todo-app/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.3" 2 | 3 | services: 4 | backend: 5 | image: docker.pkg.github.com/ltv/nodejs-course/todoapp-backend:0.1 6 | ports: 7 | - 3000:3000 8 | deploy: 9 | replicas: 12 10 | frontend: 11 | image: docker.pkg.github.com/ltv/nodejs-course/todoapp-frontend:0.1 12 | ports: 13 | - 80:80 14 | deploy: 15 | replicas: 6 16 | -------------------------------------------------------------------------------- /packages/todo-app/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "todo-app", 3 | "version": "0.1.0", 4 | "description": "Demo todo App with Express Server", 5 | "main": "index.js", 6 | "repository": "https://github.com/ltv/nodejs-course.git", 7 | "author": "Luc ", 8 | "license": "MIT", 9 | "private": true, 10 | "workspaces": [ 11 | "packages/*" 12 | ], 13 | "scripts": { 14 | "serve:frontend": "yarn workspace @app/frontend serve", 15 | "serve:backend": "yarn workspace @app/backend serve" 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/.gitignore: -------------------------------------------------------------------------------- 1 | # Created by https://www.gitignore.io/api/node,macos 2 | # Edit at https://www.gitignore.io/?templates=node,macos 3 | 4 | ### macOS ### 5 | # General 6 | .DS_Store 7 | .AppleDouble 8 | .LSOverride 9 | 10 | # Icon must end with two \r 11 | Icon 12 | 13 | # Thumbnails 14 | ._* 15 | 16 | # Files that might appear in the root of a volume 17 | .DocumentRevisions-V100 18 | .fseventsd 19 | .Spotlight-V100 20 | .TemporaryItems 21 | .Trashes 22 | .VolumeIcon.icns 23 | .com.apple.timemachine.donotpresent 24 | 25 | # Directories potentially created on remote AFP share 26 | .AppleDB 27 | .AppleDesktop 28 | Network Trash Folder 29 | Temporary Items 30 | .apdisk 31 | 32 | ### Node ### 33 | # Logs 34 | logs 35 | *.log 36 | npm-debug.log* 37 | yarn-debug.log* 38 | yarn-error.log* 39 | lerna-debug.log* 40 | 41 | # Runtime data 42 | pids 43 | *.pid 44 | *.seed 45 | *.pid.lock 46 | 47 | # Directory for instrumented libs generated by jscoverage/JSCover 48 | lib-cov 49 | 50 | # Coverage directory used by tools like istanbul 51 | coverage 52 | *.lcov 53 | 54 | # nyc test coverage 55 | .nyc_output 56 | 57 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 58 | .grunt 59 | 60 | # Bower dependency directory (https://bower.io/) 61 | bower_components 62 | 63 | # node-waf configuration 64 | .lock-wscript 65 | 66 | # Compiled binary addons (https://nodejs.org/api/addons.html) 67 | build/Release 68 | 69 | # Dependency directories 70 | node_modules/ 71 | jspm_packages/ 72 | 73 | # TypeScript v1 declaration files 74 | typings/ 75 | 76 | # TypeScript cache 77 | *.tsbuildinfo 78 | 79 | # Optional npm cache directory 80 | .npm 81 | 82 | # Optional eslint cache 83 | .eslintcache 84 | 85 | # Optional REPL history 86 | .node_repl_history 87 | 88 | # Output of 'npm pack' 89 | *.tgz 90 | 91 | # Yarn Integrity file 92 | .yarn-integrity 93 | 94 | # dotenv environment variables file 95 | .env 96 | .env.test 97 | 98 | # parcel-bundler cache (https://parceljs.org/) 99 | .cache 100 | 101 | # next.js build output 102 | .next 103 | 104 | # nuxt.js build output 105 | .nuxt 106 | 107 | # rollup.js default build output 108 | dist/ 109 | 110 | # Uncomment the public line if your project uses Gatsby 111 | # https://nextjs.org/blog/next-9-1#public-directory-support 112 | # https://create-react-app.dev/docs/using-the-public-folder/#docsNav 113 | # public 114 | 115 | # Storybook build outputs 116 | .out 117 | .storybook-out 118 | 119 | # vuepress build output 120 | .vuepress/dist 121 | 122 | # Serverless directories 123 | .serverless/ 124 | 125 | # FuseBox cache 126 | .fusebox/ 127 | 128 | # DynamoDB Local files 129 | .dynamodb/ 130 | 131 | # Temporary folders 132 | tmp/ 133 | temp/ 134 | 135 | # End of https://www.gitignore.io/api/node,macos 136 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/Dockerfile: -------------------------------------------------------------------------------- 1 | FROM node:12.16-alpine 2 | 3 | WORKDIR /app/ 4 | ADD package.json . 5 | RUN yarn install --production=true 6 | ADD src ./src 7 | 8 | CMD [ "yarn", "start" ] 9 | EXPOSE 3000 10 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/README.md: -------------------------------------------------------------------------------- 1 | # TODO App Backend 2 | 3 | ## Business 4 | 5 | ### User stories 6 | 7 | - Người dùng có thể tạo nhiều todo item 8 | - Người dùng có thể mark done 1 hoặc nhiều todo item 9 | - Người dùng có thể xem toàn bộ todo (Bao gồm đã hoàn thành và chưa hoàn thành) 10 | - Người dùng có thể filter những todo đã hoàn thành 11 | - Người dùng có thể filter những todo chưa hoàn thành 12 | - Người dùng có thể xóa các todo 13 | - Người dùng có thể xóa toàn bộ những todo đã hoàn thành 14 | - Nguoi dung có thê sưa nôi dung todo. 15 | 16 | ## Analyse 17 | 18 | ### Build APIs 19 | 20 | - Create Todo (Create single todo for each time) (`/api/createTodo`) 21 | - Update Todo & Mark this todo as completed (`/api/updateTodo`) 22 | - Get All Todo (`/api/todos`) 23 | - Get All completed todos (`/api/todos` with params: `completed: boolean`) 24 | - Get All activated todos (`/api/todos` with params: `completed: false`) 25 | - Delete Todo (`/api/deleteTodo`) 26 | - Clear Completed todos (`/api/clearCompleted`) 27 | 28 | > All api are accept JSON body, response JSON. 29 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@app/backend", 3 | "version": "0.1.0", 4 | "description": "Demo todo App with Express Server", 5 | "main": "index.js", 6 | "repository": "https://github.com/ltv/nodejs-course.git", 7 | "author": "Luc ", 8 | "license": "MIT", 9 | "private": true, 10 | "dependencies": { 11 | "body-parser": "^1.19.0", 12 | "cors": "^2.8.5", 13 | "express": "^4.17.1", 14 | "express-list-endpoints": "^4.0.1" 15 | }, 16 | "scripts": { 17 | "start": "node src/app.js", 18 | "serve": "nodemon src/app.js", 19 | "test": "jest --coverage" 20 | }, 21 | "devDependencies": { 22 | "jest": "^25.1.0", 23 | "nodemon": "^2.0.2" 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/src/app.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const bodyParser = require('body-parser'); 3 | const cors = require('cors'); 4 | const allRoutes = require('express-list-endpoints'); 5 | 6 | const router = require('./router'); 7 | 8 | const app = express(); 9 | const port = 3000; 10 | 11 | app.use(cors()); 12 | app.use(bodyParser.urlencoded({ extended: false })); 13 | app.use(bodyParser.json()); 14 | app.use('/api', router); 15 | app.get('/', (_, res) => { 16 | res.send('Hello. This is TODO App APIs'); 17 | }); 18 | 19 | app.listen(port, () => { 20 | console.log(`TODO app listening on port ${port}!`); 21 | console.log('Registered Routes: '); 22 | console.log(allRoutes(app)); 23 | }); 24 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/src/controllers/TodoController.js: -------------------------------------------------------------------------------- 1 | // - Create Todo (Create single todo for each time) (`/api/createTodo`) 2 | // - Update Todo & Mark this todo as completed (`/api/updateTodo`) 3 | // - Get All Todo (`/api/todos`) 4 | // - Get All completed todos (`/api/todos` with params: `completed: boolean`) 5 | // - Get All activated todos (`/api/todos` with params: `completed: false`) 6 | // - Delete Todo (`/api/deleteTodo`) 7 | // - Clear Completed todos (`/api/clearCompleted`) 8 | const todoModel = require('../models/todo'); 9 | 10 | // id, title, completed 11 | exports.createTodo = (req, res) => { 12 | const todo = req.body; 13 | const inserted = todoModel.insert(todo); 14 | res.json(inserted); 15 | }; 16 | 17 | exports.updateTodo = (req, res) => { 18 | const todo = req.body; 19 | const updated = todoModel.updateById(todo); 20 | res.json(updated); 21 | }; 22 | 23 | exports.getTodoList = (req, res) => { 24 | const os = require('os'); 25 | const ifaces = os.networkInterfaces(); 26 | 27 | Object.keys(ifaces).forEach(function(ifname) { 28 | let alias = 0; 29 | 30 | ifaces[ifname].forEach(function(iface) { 31 | if ('IPv4' !== iface.family || iface.internal !== false) { 32 | // skip over internal (i.e. 127.0.0.1) and non-ipv4 addresses 33 | return; 34 | } 35 | 36 | if (alias >= 1) { 37 | // this single interface has multiple ipv4 addresses 38 | console.log(ifname + ':' + alias, iface.address); 39 | } else { 40 | // this interface has only one ipv4 adress 41 | console.log(ifname, iface.address); 42 | } 43 | ++alias; 44 | }); 45 | }); 46 | const { completed } = req.body; 47 | const todos = 48 | completed === undefined 49 | ? todoModel.findAll() 50 | : todoModel.findAll({ completed: !!completed }); 51 | res.json(todos); 52 | }; 53 | 54 | exports.deleteTodo = (req, res) => { 55 | const { id } = req.body; 56 | const result = todoModel.deleteById(id); 57 | res.json({ result }); 58 | }; 59 | 60 | exports.clearCompleted = (_, res) => { 61 | const completed = todoModel.findAll({ completed: true }); 62 | completed.forEach(todo => { 63 | todoModel.deleteById(todo.id); 64 | }); 65 | res.json({ result: true }); 66 | }; 67 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/src/models/tests/todo.spec.js: -------------------------------------------------------------------------------- 1 | const { insert, updateById, deleteById, findAll, todos } = require('../todo'); 2 | describe('Test todo model', () => { 3 | it('Should create todo & return inserted todo', () => { 4 | const inserted = insert({ id: 1, title: 'First Todo' }); 5 | expect.assertions(3); 6 | expect(inserted).toEqual({ id: 1, title: 'First Todo', completed: false }); 7 | expect(todos.length).toEqual(1); 8 | expect(todos[0]).toEqual({ id: 1, title: 'First Todo', completed: false }); 9 | }); 10 | 11 | it('Should update todo & return updated one', () => { 12 | // insert({ id: 1, title: 'First Todo' }); 13 | const updated = updateById({ id: 1, title: 'Second Todo' }); 14 | expect.assertions(4); 15 | expect(todos[0]).toEqual(updated); 16 | expect(updated.id).toEqual(1); 17 | expect(updated.title).toEqual('Second Todo'); 18 | expect(updated.completed).toEqual(false); 19 | }); 20 | 21 | it('Should return false if could not found todo item', () => { 22 | // insert({ id: 1, title: 'First Todo' }); 23 | const updated = updateById({ id: 2, title: 'Second Todo' }); 24 | expect(updated).toEqual(false); 25 | }); 26 | 27 | it('Should delete todo with id', () => { 28 | // insert({ id: 1, title: 'First Todo' }); 29 | const deleted = deleteById(1); 30 | expect.assertions(2); 31 | expect(deleted).toEqual(true); 32 | expect(todos.length).toEqual(0); 33 | }); 34 | 35 | it('Should return false if could not found todo id', () => { 36 | insert({ id: 1, title: 'First Todo' }); 37 | const deleted = deleteById(2); 38 | expect.assertions(2); 39 | expect(deleted).toEqual(false); 40 | expect(todos.length).toEqual(1); 41 | }); 42 | 43 | it('Should return all todos', () => { 44 | insert({ id: 2, title: 'Second Todo' }); 45 | const todoList = findAll(); 46 | expect(todoList).toEqual(todos); 47 | }); 48 | 49 | it('Should return all activated todos', () => { 50 | insert({ id: 3, title: 'Third Todo' }); 51 | insert({ id: 4, title: '4th Todo' }); 52 | updateById({ id: 2, completed: true }); 53 | const todoList = findAll({ completed: false }); 54 | expect(todoList.length).toEqual(3); 55 | }); 56 | 57 | it('Should return all completed todos', () => { 58 | const todoList = findAll({ completed: true }); 59 | expect(todoList.length).toEqual(1); 60 | }); 61 | }); 62 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/src/models/todo.js: -------------------------------------------------------------------------------- 1 | const todos = []; 2 | 3 | /** 4 | * Insert todo to database 5 | * @todo {id, title, completed} 6 | */ 7 | exports.insert = todo => { 8 | const tobeTodo = { ...todo, completed: false }; 9 | todos.push(tobeTodo); 10 | return tobeTodo; 11 | }; 12 | 13 | /** 14 | * update todo by Id 15 | * @param { todo: Todo } 16 | * @return { todo | false } 17 | */ 18 | exports.updateById = todo => { 19 | let todoIdx = todos.findIndex(t => t.id === todo.id); // Todo || undefined 20 | if (todoIdx !== -1) { 21 | todos[todoIdx] = { ...todos[todoIdx], ...todo }; 22 | return todos[todoIdx]; 23 | } else { 24 | return false; 25 | } 26 | }; 27 | 28 | /** 29 | * Delete todo by Id 30 | * @param { id: number } 31 | * @return { boolean } 32 | */ 33 | exports.deleteById = id => { 34 | const todoIdx = todos.findIndex(todo => todo.id === id); // index of todo || -1 35 | if (todoIdx === -1) { 36 | return false; 37 | } 38 | 39 | todos.splice(todoIdx, 1); // delete. Call mongodb, ... 40 | return true; 41 | }; 42 | 43 | /** 44 | * find all todos 45 | * @params {completed} 46 | * @return { list } 47 | */ 48 | exports.findAll = params => { 49 | if (!params) { 50 | return todos; 51 | } else { 52 | const { completed } = params; 53 | return todos.filter(p => p.completed === completed); 54 | } 55 | }; 56 | 57 | exports.todos = todos; 58 | -------------------------------------------------------------------------------- /packages/todo-app/packages/backend/src/router/index.js: -------------------------------------------------------------------------------- 1 | const express = require('express'); 2 | const router = express.Router(); 3 | const { 4 | createTodo, 5 | updateTodo, 6 | getTodoList, 7 | clearCompleted, 8 | deleteTodo 9 | } = require('../controllers/TodoController'); 10 | 11 | // - Create Todo (Create single todo for each time) (`/api/createTodo`) 12 | // - Update Todo & Mark this todo as completed (`/api/updateTodo`) 13 | // - Get All Todo (`/api/todos`) 14 | // - Get All completed todos (`/api/todos` with params: `completed: boolean`) 15 | // - Get All activated todos (`/api/todos` with params: `completed: false`) 16 | // - Delete Todo (`/api/deleteTodo`) 17 | // - Clear Completed todos (`/api/clearCompleted`) 18 | 19 | router.post('/createTodo', createTodo); 20 | router.post('/updateTodo', updateTodo); 21 | router.post('/todos', getTodoList); 22 | router.post('/deleteTodo', deleteTodo); 23 | router.post('/clearCompleted', clearCompleted); 24 | 25 | module.exports = router; 26 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/.browserslistrc: -------------------------------------------------------------------------------- 1 | > 1% 2 | last 2 versions 3 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | tests 3 | dist 4 | .vscode 5 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/.env.production: -------------------------------------------------------------------------------- 1 | VUE_APP_SERVICE_ENDPOINT=http://todo.midujs.io:3000/api 2 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | /tests/e2e/videos/ 6 | /tests/e2e/screenshots/ 7 | 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | 12 | # Log files 13 | npm-debug.log* 14 | yarn-debug.log* 15 | yarn-error.log* 16 | 17 | # Editor directories and files 18 | .idea 19 | .vscode 20 | *.suo 21 | *.ntvs* 22 | *.njsproj 23 | *.sln 24 | *.sw? 25 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "useTabs": false, 3 | "printWidth": 80, 4 | "tabWidth": 2, 5 | "singleQuote": true, 6 | "trailingComma": "all", 7 | "jsxBracketSameLine": false, 8 | "parser": "typescript", 9 | "overrides": [ 10 | { 11 | "files": "*.vue", 12 | "options": { 13 | "parser": "vue" 14 | } 15 | } 16 | ] 17 | } 18 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | # ===== Builder ===== 3 | # =================== 4 | FROM node:10-alpine AS builder 5 | 6 | RUN apk --no-cache add \ 7 | g++ make python git \ 8 | && yarn global add node-gyp \ 9 | && rm -rf /var/cache/apk/* 10 | 11 | WORKDIR /builder/ 12 | 13 | # Cache client's package 14 | ADD package.json . 15 | ADD yarn.lock . 16 | RUN yarn --pure-lockfile 17 | 18 | # Build 19 | ADD .env.production . 20 | ADD . . 21 | RUN yarn build --mode production 22 | 23 | # ===== Image ===== 24 | # ================== 25 | 26 | ## Client Image 27 | FROM nginx:alpine AS frontend 28 | COPY nginx.conf /etc/nginx/nginx.conf 29 | COPY --from=builder /builder/dist/ /usr/share/nginx/html 30 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/README.md: -------------------------------------------------------------------------------- 1 | # todo-app 2 | 3 | ## Project setup 4 | ``` 5 | yarn install 6 | ``` 7 | 8 | ### Compiles and hot-reloads for development 9 | ``` 10 | yarn serve 11 | ``` 12 | 13 | ### Compiles and minifies for production 14 | ``` 15 | yarn build 16 | ``` 17 | 18 | ### Run your unit tests 19 | ``` 20 | yarn test:unit 21 | ``` 22 | 23 | ### Run your end-to-end tests 24 | ``` 25 | yarn test:e2e 26 | ``` 27 | 28 | ### Lints and fixes files 29 | ``` 30 | yarn lint 31 | ``` 32 | 33 | ### Customize configuration 34 | See [Configuration Reference](https://cli.vuejs.org/config/). 35 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/cypress.json: -------------------------------------------------------------------------------- 1 | { 2 | "pluginsFile": "tests/e2e/plugins/index.js" 3 | } 4 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/jest.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | preset: '@vue/cli-plugin-unit-jest/presets/typescript' 3 | } 4 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; 2 | worker_processes 1; 3 | 4 | error_log /var/log/nginx/error.log warn; 5 | pid /var/run/nginx.pid; 6 | 7 | events { 8 | worker_connections 1024; 9 | } 10 | 11 | http { 12 | include /etc/nginx/mime.types; 13 | default_type application/octet-stream; 14 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 15 | '$status $body_bytes_sent "$http_referer" ' 16 | '"$http_user_agent" "$http_x_forwarded_for"'; 17 | 18 | gzip on; 19 | sendfile on; 20 | tcp_nopush on; 21 | keepalive_timeout 65; 22 | access_log /var/log/nginx/access.log main; 23 | 24 | server { 25 | listen 80; 26 | 27 | root /usr/share/nginx/html/; 28 | index index.html; 29 | 30 | location ~ ^/(assets)/ { 31 | root /usr/share/nginx/html/; 32 | } 33 | 34 | location ~ \.(js|ico|html)$ { 35 | root /usr/share/nginx/html/; 36 | } 37 | 38 | location ~ / { 39 | try_files /index.html = 404; 40 | } 41 | } 42 | } 43 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@app/frontend", 3 | "description": "Todo App", 4 | "version": "0.1.0", 5 | "private": true, 6 | "scripts": { 7 | "serve": "vue-cli-service serve", 8 | "build": "vue-cli-service build", 9 | "test:unit": "vue-cli-service test:unit", 10 | "test:e2e": "vue-cli-service test:e2e", 11 | "lint": "vue-cli-service lint" 12 | }, 13 | "dependencies": { 14 | "axios": "^0.21.2", 15 | "vue": "^2.6.11", 16 | "vue-class-component": "^7.2.2", 17 | "vue-property-decorator": "^8.3.0", 18 | "vue-router": "^3.1.5", 19 | "vuex": "^3.1.2" 20 | }, 21 | "devDependencies": { 22 | "@types/jest": "^24.0.19", 23 | "@vue/cli-plugin-e2e-cypress": "~4.2.0", 24 | "@vue/cli-plugin-router": "~4.2.0", 25 | "@vue/cli-plugin-typescript": "~4.2.0", 26 | "@vue/cli-plugin-unit-jest": "~4.2.0", 27 | "@vue/cli-plugin-vuex": "~4.2.0", 28 | "@vue/cli-service": "~4.2.0", 29 | "@vue/test-utils": "1.0.0-beta.31", 30 | "sass": "^1.25.0", 31 | "sass-loader": "^8.0.2", 32 | "typescript": "~3.7.5", 33 | "vue-template-compiler": "^2.6.11" 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/todo-app/packages/frontend/public/favicon.ico -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | <%= htmlWebpackPlugin.options.title %> 9 | 10 | 11 | 14 |
15 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 21 | 22 | 25 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/todo-app/packages/frontend/src/assets/logo.png -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/assets/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body { 3 | margin: 0; 4 | padding: 0; 5 | } 6 | 7 | button { 8 | margin: 0; 9 | padding: 0; 10 | border: 0; 11 | background: none; 12 | font-size: 100%; 13 | vertical-align: baseline; 14 | font-family: inherit; 15 | font-weight: inherit; 16 | color: inherit; 17 | -webkit-appearance: none; 18 | appearance: none; 19 | -webkit-font-smoothing: antialiased; 20 | -moz-osx-font-smoothing: grayscale; 21 | } 22 | 23 | body { 24 | font: 14px 'Helvetica Neue', Helvetica, Arial, sans-serif; 25 | line-height: 1.4em; 26 | background: #f5f5f5; 27 | color: #4d4d4d; 28 | min-width: 230px; 29 | max-width: 550px; 30 | margin: 0 auto; 31 | -webkit-font-smoothing: antialiased; 32 | -moz-osx-font-smoothing: grayscale; 33 | font-weight: 300; 34 | } 35 | 36 | :focus { 37 | outline: 0; 38 | } 39 | 40 | .hidden { 41 | display: none; 42 | } 43 | 44 | .todoapp { 45 | background: #fff; 46 | margin: 130px 0 40px 0; 47 | position: relative; 48 | box-shadow: 0 2px 4px 0 rgba(0, 0, 0, 0.2), 0 25px 50px 0 rgba(0, 0, 0, 0.1); 49 | } 50 | 51 | .todoapp input::-webkit-input-placeholder { 52 | font-style: italic; 53 | font-weight: 300; 54 | color: #e6e6e6; 55 | } 56 | 57 | .todoapp input::-moz-placeholder { 58 | font-style: italic; 59 | font-weight: 300; 60 | color: #e6e6e6; 61 | } 62 | 63 | .todoapp input::input-placeholder { 64 | font-style: italic; 65 | font-weight: 300; 66 | color: #e6e6e6; 67 | } 68 | 69 | .todoapp h1 { 70 | position: absolute; 71 | top: -155px; 72 | width: 100%; 73 | font-size: 100px; 74 | font-weight: 100; 75 | text-align: center; 76 | color: rgba(175, 47, 47, 0.15); 77 | -webkit-text-rendering: optimizeLegibility; 78 | -moz-text-rendering: optimizeLegibility; 79 | text-rendering: optimizeLegibility; 80 | } 81 | 82 | .new-todo, 83 | .edit { 84 | position: relative; 85 | margin: 0; 86 | width: 100%; 87 | font-size: 24px; 88 | font-family: inherit; 89 | font-weight: inherit; 90 | line-height: 1.4em; 91 | border: 0; 92 | color: inherit; 93 | padding: 6px; 94 | border: 1px solid #999; 95 | box-shadow: inset 0 -1px 5px 0 rgba(0, 0, 0, 0.2); 96 | box-sizing: border-box; 97 | -webkit-font-smoothing: antialiased; 98 | -moz-osx-font-smoothing: grayscale; 99 | } 100 | 101 | .new-todo { 102 | padding: 16px 16px 16px 60px; 103 | border: none; 104 | background: rgba(0, 0, 0, 0.003); 105 | box-shadow: inset 0 -2px 1px rgba(0, 0, 0, 0.03); 106 | } 107 | 108 | .main { 109 | position: relative; 110 | z-index: 2; 111 | border-top: 1px solid #e6e6e6; 112 | } 113 | 114 | .toggle-all { 115 | width: 1px; 116 | height: 1px; 117 | border: none; 118 | /* Mobile Safari */ 119 | opacity: 0; 120 | position: absolute; 121 | right: 100%; 122 | bottom: 100%; 123 | } 124 | 125 | .toggle-all + label { 126 | width: 60px; 127 | height: 34px; 128 | font-size: 0; 129 | position: absolute; 130 | top: -52px; 131 | left: -13px; 132 | -webkit-transform: rotate(90deg); 133 | transform: rotate(90deg); 134 | } 135 | 136 | .toggle-all + label:before { 137 | content: '❯'; 138 | font-size: 22px; 139 | color: #e6e6e6; 140 | padding: 10px 27px 10px 27px; 141 | } 142 | 143 | .toggle-all:checked + label:before { 144 | color: #737373; 145 | } 146 | 147 | .todo-list { 148 | margin: 0; 149 | padding: 0; 150 | list-style: none; 151 | } 152 | 153 | .todo-list li { 154 | position: relative; 155 | font-size: 24px; 156 | border-bottom: 1px solid #ededed; 157 | } 158 | 159 | .todo-list li:last-child { 160 | border-bottom: none; 161 | } 162 | 163 | .todo-list li.editing { 164 | border-bottom: none; 165 | padding: 0; 166 | } 167 | 168 | .todo-list li.editing .edit { 169 | display: block; 170 | width: calc(100% - 43px); 171 | padding: 12px 16px; 172 | margin: 0 0 0 43px; 173 | } 174 | 175 | .todo-list li.editing .view { 176 | display: none; 177 | } 178 | 179 | .todo-list li .toggle { 180 | text-align: center; 181 | width: 40px; 182 | /* auto, since non-WebKit browsers doesn't support input styling */ 183 | height: auto; 184 | position: absolute; 185 | top: 0; 186 | bottom: 0; 187 | margin: auto 0; 188 | border: none; 189 | /* Mobile Safari */ 190 | -webkit-appearance: none; 191 | appearance: none; 192 | } 193 | 194 | .todo-list li .toggle { 195 | opacity: 0; 196 | } 197 | 198 | .todo-list li .toggle + label { 199 | /* 200 | Firefox requires `#` to be escaped - https://bugzilla.mozilla.org/show_bug.cgi?id=922433 201 | IE and Edge requires *everything* to be escaped to render, so we do that instead of just the `#` - https://developer.microsoft.com/en-us/microsoft-edge/platform/issues/7157459/ 202 | */ 203 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23ededed%22%20stroke-width%3D%223%22/%3E%3C/svg%3E'); 204 | background-repeat: no-repeat; 205 | background-position: center left; 206 | } 207 | 208 | .todo-list li .toggle:checked + label { 209 | background-image: url('data:image/svg+xml;utf8,%3Csvg%20xmlns%3D%22http%3A//www.w3.org/2000/svg%22%20width%3D%2240%22%20height%3D%2240%22%20viewBox%3D%22-10%20-18%20100%20135%22%3E%3Ccircle%20cx%3D%2250%22%20cy%3D%2250%22%20r%3D%2250%22%20fill%3D%22none%22%20stroke%3D%22%23bddad5%22%20stroke-width%3D%223%22/%3E%3Cpath%20fill%3D%22%235dc2af%22%20d%3D%22M72%2025L42%2071%2027%2056l-4%204%2020%2020%2034-52z%22/%3E%3C/svg%3E'); 210 | } 211 | 212 | .todo-list li label { 213 | word-break: break-all; 214 | padding: 15px 15px 15px 60px; 215 | display: block; 216 | line-height: 1.2; 217 | transition: color 0.4s; 218 | } 219 | 220 | .todo-list li.completed label { 221 | color: #d9d9d9; 222 | text-decoration: line-through; 223 | } 224 | 225 | .todo-list li .destroy { 226 | display: none; 227 | position: absolute; 228 | top: 0; 229 | right: 10px; 230 | bottom: 0; 231 | width: 40px; 232 | height: 40px; 233 | margin: auto 0; 234 | font-size: 30px; 235 | color: #cc9a9a; 236 | margin-bottom: 11px; 237 | transition: color 0.2s ease-out; 238 | } 239 | 240 | .todo-list li .destroy:hover { 241 | color: #af5b5e; 242 | } 243 | 244 | .todo-list li .destroy:after { 245 | content: '×'; 246 | } 247 | 248 | .todo-list li:hover .destroy { 249 | display: block; 250 | } 251 | 252 | .todo-list li .edit { 253 | display: none; 254 | } 255 | 256 | .todo-list li.editing:last-child { 257 | margin-bottom: -1px; 258 | } 259 | 260 | .footer { 261 | color: #777; 262 | padding: 10px 15px; 263 | height: 20px; 264 | text-align: center; 265 | border-top: 1px solid #e6e6e6; 266 | } 267 | 268 | .footer:before { 269 | content: ''; 270 | position: absolute; 271 | right: 0; 272 | bottom: 0; 273 | left: 0; 274 | height: 50px; 275 | overflow: hidden; 276 | box-shadow: 0 1px 1px rgba(0, 0, 0, 0.2), 0 8px 0 -3px #f6f6f6, 277 | 0 9px 1px -3px rgba(0, 0, 0, 0.2), 0 16px 0 -6px #f6f6f6, 278 | 0 17px 2px -6px rgba(0, 0, 0, 0.2); 279 | } 280 | 281 | .todo-count { 282 | float: left; 283 | text-align: left; 284 | } 285 | 286 | .todo-count strong { 287 | font-weight: 300; 288 | } 289 | 290 | .filters { 291 | margin: 0; 292 | padding: 0; 293 | list-style: none; 294 | position: absolute; 295 | right: 0; 296 | left: 0; 297 | } 298 | 299 | .filters li { 300 | display: inline; 301 | } 302 | 303 | .filters li a { 304 | color: inherit; 305 | margin: 3px; 306 | padding: 3px 7px; 307 | text-decoration: none; 308 | border: 1px solid transparent; 309 | border-radius: 3px; 310 | } 311 | 312 | .filters li a:hover { 313 | border-color: rgba(175, 47, 47, 0.1); 314 | } 315 | 316 | .filters li a.selected { 317 | border-color: rgba(175, 47, 47, 0.2); 318 | } 319 | 320 | .clear-completed, 321 | html .clear-completed:active { 322 | float: right; 323 | position: relative; 324 | line-height: 20px; 325 | text-decoration: none; 326 | cursor: pointer; 327 | } 328 | 329 | .clear-completed:hover { 330 | text-decoration: underline; 331 | } 332 | 333 | .info { 334 | margin: 65px auto 0; 335 | color: #bfbfbf; 336 | font-size: 10px; 337 | text-shadow: 0 1px 0 rgba(255, 255, 255, 0.5); 338 | text-align: center; 339 | } 340 | 341 | .info p { 342 | line-height: 1; 343 | } 344 | 345 | .info a { 346 | color: inherit; 347 | text-decoration: none; 348 | font-weight: 400; 349 | } 350 | 351 | .info a:hover { 352 | text-decoration: underline; 353 | } 354 | 355 | /* 356 | Hack to remove background from Mobile Safari. 357 | Can't use it globally since it destroys checkboxes in Firefox 358 | */ 359 | @media screen and (-webkit-min-device-pixel-ratio: 0) { 360 | .toggle-all, 361 | .todo-list li .toggle { 362 | background: none; 363 | } 364 | 365 | .todo-list li .toggle { 366 | height: 40px; 367 | } 368 | } 369 | 370 | @media (max-width: 430px) { 371 | .footer { 372 | height: 50px; 373 | } 374 | 375 | .filters { 376 | bottom: 10px; 377 | } 378 | } 379 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/components/AppFooter.vue: -------------------------------------------------------------------------------- 1 | 18 | 25 | 26 | 28 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/main.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import App from './App.vue'; 3 | import router from './router'; 4 | import store from './store'; 5 | 6 | Vue.config.productionTip = false; 7 | 8 | new Vue({ 9 | router, 10 | store, 11 | render: (h) => h(App), 12 | }).$mount('#app'); 13 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/router/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import VueRouter from 'vue-router'; 3 | import Home from '../views/Home/index.vue'; 4 | 5 | Vue.use(VueRouter); 6 | 7 | const routes = [ 8 | { 9 | path: '/', 10 | name: 'Home', 11 | component: Home, 12 | }, 13 | ]; 14 | 15 | const router = new VueRouter({ 16 | mode: 'history', 17 | base: process.env.BASE_URL, 18 | routes, 19 | }); 20 | 21 | export default router; 22 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/service/index.ts: -------------------------------------------------------------------------------- 1 | import axios, { AxiosInstance, AxiosRequestConfig, Method } from 'axios'; 2 | // axios 3 | axios.defaults.headers.post['Content-Type'] = 'application/json'; 4 | 5 | export interface ServiceGraphQLOptions { 6 | response?: { 7 | raw?: boolean; 8 | }; 9 | } 10 | 11 | export interface ServiceOptions { 12 | namespace?: string; 13 | graphql?: ServiceGraphQLOptions; 14 | } 15 | 16 | export default class Service { 17 | private axios: AxiosInstance; 18 | private headers: any; 19 | // Create apollo client 20 | private defaultOptions: ServiceOptions = { 21 | namespace: undefined, 22 | graphql: { 23 | response: { 24 | raw: false, 25 | }, 26 | }, 27 | }; 28 | /** 29 | * Creates an instance of Service. 30 | * 31 | * @memberOf Service 32 | */ 33 | constructor(options?: ServiceOptions) { 34 | this.defaultOptions = { ...this.defaultOptions, ...options }; 35 | const { namespace = null } = this.defaultOptions; 36 | // Accept */* 37 | axios.defaults.headers.common.Accept = '*/*'; 38 | const endpoint = 39 | process.env.VUE_APP_SERVICE_ENDPOINT || 'http://localhost:3000/api'; 40 | const baseURL = endpoint + (namespace ? `/${namespace}/` : '/'); 41 | this.axios = axios.create({ 42 | baseURL, 43 | responseType: 'json', 44 | headers: { 'Access-Control-Allow-Origin': '*' }, 45 | }); 46 | } 47 | 48 | public withHeader(headers: any): Service { 49 | this.headers = headers; 50 | return this; 51 | } 52 | 53 | public toQueryString(obj: any) { 54 | const parts = []; 55 | for (const i in obj) { 56 | if (obj.hasOwnProperty(i)) { 57 | parts.push(encodeURIComponent(i) + '=' + encodeURIComponent(obj[i])); 58 | } 59 | } 60 | return parts.join('&'); 61 | } 62 | 63 | /** 64 | * Call a service action via REST API 65 | * 66 | * @param {any} action name of action 67 | * @param {any} params parameters to request 68 | * @returns {Promise} 69 | * 70 | * @memberOf Service 71 | */ 72 | public async rest( 73 | action: string, 74 | params?: any, 75 | options = { 76 | headers: {}, 77 | method: 'post', 78 | }, 79 | ) { 80 | const { headers } = options; 81 | try { 82 | const opts: AxiosRequestConfig = { 83 | url: action, 84 | method: options.method as Method, 85 | data: params, 86 | headers: { 87 | ...(this.headers || {}), 88 | ...headers, 89 | }, 90 | }; 91 | const response = await this.axios.request(opts); 92 | return response.data; 93 | } catch (err) { 94 | throw err; 95 | } 96 | } 97 | 98 | public get(action: string, params?: any, options: any = {}) { 99 | const { headers = {} } = options; 100 | const query = this.toQueryString(params); 101 | const path = query ? `${action}?${query}` : action; 102 | return this.rest( 103 | path, 104 | {}, 105 | { 106 | method: 'get', 107 | headers, 108 | }, 109 | ); 110 | } 111 | 112 | public post(action: string, params?: any, options: any = {}) { 113 | const { headers = {} } = options; 114 | return this.rest(action, params, { 115 | method: 'post', 116 | headers, 117 | }); 118 | } 119 | } 120 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/shims-tsx.d.ts: -------------------------------------------------------------------------------- 1 | import Vue, { VNode } from 'vue'; 2 | 3 | declare global { 4 | namespace JSX { 5 | // tslint:disable no-empty-interface 6 | interface Element extends VNode {} 7 | // tslint:disable no-empty-interface 8 | interface ElementClass extends Vue {} 9 | interface IntrinsicElements { 10 | [elem: string]: any; 11 | } 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import Vue from 'vue'; 3 | export default Vue; 4 | } 5 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/store/index.ts: -------------------------------------------------------------------------------- 1 | import Vue from 'vue'; 2 | import Vuex from 'vuex'; 3 | 4 | import { HomeService } from '../views/Home/HomeService'; 5 | 6 | const service = new HomeService(); 7 | 8 | Vue.use(Vuex); 9 | 10 | export interface Todo { 11 | id: number; 12 | title: string; 13 | completed?: boolean; 14 | } 15 | 16 | interface RootState { 17 | todos: Todo[]; 18 | visibility: 'all' | 'active' | 'completed'; 19 | } 20 | 21 | export default new Vuex.Store({ 22 | state: { 23 | todos: [], 24 | visibility: 'all', 25 | }, 26 | mutations: { 27 | ADD_NEW_TODO(state, todo) { 28 | const { todos } = state; 29 | todos.push(todo); 30 | state.todos = [...todos]; 31 | }, 32 | SET_ALL_DONE(state) { 33 | state.todos.forEach((todo: Todo) => { 34 | todo.completed = true; 35 | }); 36 | }, 37 | REMOVE_TODO(state, todo) { 38 | state.todos.splice(state.todos.indexOf(todo), 1); 39 | }, 40 | REMOVE_COMPLETED(state) { 41 | state.todos = state.todos.filter((todo: any) => !todo.completed); 42 | }, 43 | UPDATE_TODO(state, todo: Todo) { 44 | const { todos } = state; 45 | const todoIdx: number = todos.findIndex((t: Todo) => t.id === todo.id); 46 | if (todoIdx >= 0) { 47 | todos[todoIdx] = { ...todo }; 48 | } 49 | }, 50 | SET_TODO_LIST(state, todos: Todo[]) { 51 | state.todos = [...todos]; 52 | }, 53 | }, 54 | actions: { 55 | async addNewTodo({ commit, state }, title: string) { 56 | // commit('ADD_NEW_TODO', title); 57 | // call API add new Todo. Add ok -> push store. Failed -> Alert Error. 58 | const { todos } = state; 59 | const todo = await service.createTodo({ 60 | id: todos.length + 1, 61 | title, 62 | }); 63 | if (todo) { 64 | commit('ADD_NEW_TODO', todo); 65 | } 66 | }, 67 | setAllDone({ commit }) { 68 | commit('SET_ALL_DONE'); 69 | }, 70 | async removeTodo({ commit }, todo: Todo) { 71 | const delResult = service.deleteTodo(todo.id); 72 | if (delResult) { 73 | commit('REMOVE_TODO', todo); 74 | } 75 | }, 76 | removeCompleted({ commit }) { 77 | commit('REMOVE_COMPLETED'); 78 | }, 79 | async updateTodo({ commit }, todo: Todo) { 80 | // commit('UPDATE_TODO', todo); 81 | const updated = await service.updateTodo(todo); 82 | if (updated) { 83 | commit('UPDATE_TODO', updated); 84 | } 85 | }, 86 | async getTodoList({ commit }, completed) { 87 | const todos = await service.getTodoList(completed); 88 | commit('SET_TODO_LIST', todos); 89 | }, 90 | }, 91 | getters: { 92 | all(state) { 93 | return state.todos; 94 | }, 95 | active(state) { 96 | return state.todos.filter((todo: any) => !todo.completed); 97 | }, 98 | completed(state) { 99 | return state.todos.filter((todo: any) => todo.completed); 100 | }, 101 | }, 102 | modules: {}, 103 | }); 104 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/views/Home/HomeService.ts: -------------------------------------------------------------------------------- 1 | import Service from '@/service'; 2 | import { Todo } from '@/store'; 3 | 4 | export class HomeService extends Service { 5 | public getTodoList(completed?: boolean): Promise { 6 | // return [{ id: 1, title: 'My First Todo', completed: false }]; 7 | let params = {}; 8 | if (completed !== undefined && completed !== null) { 9 | params = { completed: !!completed }; // completed = '' -> false, completed = 0 -> false, completed = 1 "abc" 10 | } 11 | return this.post('/todos', params); 12 | } 13 | 14 | public createTodo(todo: Todo): Promise { 15 | return this.post('/createTodo', todo); 16 | } 17 | 18 | public updateTodo(todo: Todo): Promise { 19 | return this.post('/updateTodo', todo); 20 | } 21 | 22 | public deleteTodo(id: number): Promise { 23 | return this.post('/deleteTodo', { id }); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/src/views/Home/index.vue: -------------------------------------------------------------------------------- 1 | 70 | 71 | 194 | 195 | 197 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/tests/e2e/plugins/index.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable arrow-body-style */ 2 | // https://docs.cypress.io/guides/guides/plugins-guide.html 3 | 4 | // if you need a custom webpack configuration you can uncomment the following import 5 | // and then use the `file:preprocessor` event 6 | // as explained in the cypress docs 7 | // https://docs.cypress.io/api/plugins/preprocessors-api.html#Examples 8 | 9 | // /* eslint-disable import/no-extraneous-dependencies, global-require */ 10 | // const webpack = require('@cypress/webpack-preprocessor') 11 | 12 | module.exports = (on, config) => { 13 | // on('file:preprocessor', webpack({ 14 | // webpackOptions: require('@vue/cli-service/webpack.config'), 15 | // watchOptions: {} 16 | // })) 17 | 18 | return Object.assign({}, config, { 19 | fixturesFolder: 'tests/e2e/fixtures', 20 | integrationFolder: 'tests/e2e/specs', 21 | screenshotsFolder: 'tests/e2e/screenshots', 22 | videosFolder: 'tests/e2e/videos', 23 | supportFile: 'tests/e2e/support/index.js' 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/tests/e2e/specs/test.js: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe('My First Test', () => { 4 | it('Visits the app root url', () => { 5 | cy.visit('/') 6 | cy.contains('h1', 'Welcome to Your Vue.js + TypeScript App') 7 | }) 8 | }) 9 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/tests/e2e/support/commands.js: -------------------------------------------------------------------------------- 1 | // *********************************************** 2 | // This example commands.js shows you how to 3 | // create various custom commands and overwrite 4 | // existing commands. 5 | // 6 | // For more comprehensive examples of custom 7 | // commands please read more here: 8 | // https://on.cypress.io/custom-commands 9 | // *********************************************** 10 | // 11 | // 12 | // -- This is a parent command -- 13 | // Cypress.Commands.add("login", (email, password) => { ... }) 14 | // 15 | // 16 | // -- This is a child command -- 17 | // Cypress.Commands.add("drag", { prevSubject: 'element'}, (subject, options) => { ... }) 18 | // 19 | // 20 | // -- This is a dual command -- 21 | // Cypress.Commands.add("dismiss", { prevSubject: 'optional'}, (subject, options) => { ... }) 22 | // 23 | // 24 | // -- This is will overwrite an existing command -- 25 | // Cypress.Commands.overwrite("visit", (originalFn, url, options) => { ... }) 26 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/tests/e2e/support/index.js: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import './commands' 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/tests/unit/example.spec.ts: -------------------------------------------------------------------------------- 1 | import { shallowMount } from '@vue/test-utils'; 2 | import HelloWorld from '@/components/HelloWorld.vue'; 3 | 4 | describe('HelloWorld.vue', () => { 5 | it('renders props.msg when passed', () => { 6 | const msg = 'new message'; 7 | const wrapper = shallowMount(HelloWorld, { 8 | propsData: { msg }, 9 | }); 10 | expect(wrapper.text()).toMatch(msg); 11 | }); 12 | }); 13 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es5", 4 | "module": "esnext", 5 | "strict": true, 6 | "jsx": "preserve", 7 | "importHelpers": true, 8 | "moduleResolution": "node", 9 | "experimentalDecorators": true, 10 | "esModuleInterop": true, 11 | "allowSyntheticDefaultImports": true, 12 | "sourceMap": true, 13 | "baseUrl": ".", 14 | "types": [ 15 | "webpack-env", 16 | "jest" 17 | ], 18 | "paths": { 19 | "@/*": [ 20 | "src/*" 21 | ] 22 | }, 23 | "lib": [ 24 | "esnext", 25 | "dom", 26 | "dom.iterable", 27 | "scripthost" 28 | ] 29 | }, 30 | "include": [ 31 | "src/**/*.ts", 32 | "src/**/*.tsx", 33 | "src/**/*.vue", 34 | "tests/**/*.ts", 35 | "tests/**/*.tsx" 36 | ], 37 | "exclude": [ 38 | "node_modules" 39 | ] 40 | } 41 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "defaultSeverity": "warning", 3 | "extends": [ 4 | "tslint:recommended" 5 | ], 6 | "linterOptions": { 7 | "exclude": [ 8 | "node_modules/**" 9 | ] 10 | }, 11 | "rules": { 12 | "indent": [true, "spaces", 2], 13 | "interface-name": false, 14 | "no-consecutive-blank-lines": false, 15 | "object-literal-sort-keys": false, 16 | "ordered-imports": false, 17 | "quotemark": [true, "single"] 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/todo-app/packages/frontend/vue.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | lintOnSave: true, 3 | outputDir: 'dist', 4 | assetsDir: 'assets', 5 | productionSourceMap: false, 6 | configureWebpack: { 7 | performance: { 8 | hints: false, 9 | }, 10 | optimization: { 11 | splitChunks: { 12 | minSize: 10000, 13 | maxSize: 250000, 14 | }, 15 | }, 16 | }, 17 | devServer: { 18 | // proxy: process.env.VUE_APP_SERVICE_ENDPOINT || 'http://localhost:3000' 19 | }, 20 | }; 21 | -------------------------------------------------------------------------------- /packages/web/README.md: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/ltv/nodejs-course/1911fbf211822c278555b38077d7dd9493b5f73a/packages/web/README.md -------------------------------------------------------------------------------- /provision/.gitignore: -------------------------------------------------------------------------------- 1 | data/ 2 | -------------------------------------------------------------------------------- /provision/clean.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 'DOCKER VERSION' 3 | docker -v 4 | 5 | echo 'DOCKER COMPOSE VERSION' 6 | docker-compose -v 7 | 8 | echo 'CREATE DATA DIRECTORY' 9 | mkdir -p ./data 10 | 11 | echo 'COMPOSE DOWN' 12 | docker-compose --project-name=ltv down 13 | -------------------------------------------------------------------------------- /provision/docker-compose.yml: -------------------------------------------------------------------------------- 1 | version: "3.6" 2 | services: 3 | postgres105: 4 | image: postgres:10.5 5 | container_name: postgres105 6 | hostname: postgres105 7 | restart: always 8 | volumes: 9 | - postgres105:/var/lib/postgresql/data 10 | ports: 11 | - 5432:5432 12 | networks: 13 | - ltv 14 | environment: 15 | POSTGRES_USER: ltv 16 | POSTGRES_PASSWORD: Ltv!@#456 17 | POSTGRES_DB: ltv 18 | 19 | pgadmin4: 20 | image: dpage/pgadmin4 21 | container_name: pgadmin4 22 | restart: always 23 | volumes: 24 | - ./data/pgadmin4:/var/lib/pgadmin 25 | ports: 26 | - 5433:80 27 | networks: 28 | - ltv 29 | environment: 30 | PGADMIN_DEFAULT_EMAIL: "admin@ltv.vn" 31 | PGADMIN_DEFAULT_PASSWORD: "Ltv!@#456" 32 | 33 | nats: 34 | image: nats:1.3.0-linux 35 | container_name: nats 36 | restart: always 37 | ports: 38 | - 4222:4222 39 | - 4444:4444 40 | - 6222:6222 41 | - 8222:8222 42 | networks: 43 | - ltv 44 | 45 | redis: 46 | image: redis:5-alpine 47 | container_name: redis5 48 | hostname: redis5 49 | restart: always 50 | ports: 51 | - 6379:6379 52 | networks: 53 | - ltv 54 | 55 | redis-commander: 56 | image: rediscommander/redis-commander 57 | container_name: redis-commander 58 | hostname: redis-commander 59 | restart: always 60 | ports: 61 | - 6380:8081 62 | networks: 63 | - ltv 64 | environment: 65 | REDIS_HOSTS: local:redis5:6379 66 | 67 | volumes: 68 | pgadmin4: 69 | postgres105: 70 | 71 | networks: 72 | ltv: 73 | external: true 74 | -------------------------------------------------------------------------------- /provision/get-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | set -e 3 | 4 | # This script is meant for quick & easy install via: 5 | # $ curl -fsSL https://get.docker.com -o get-docker.sh 6 | # $ sh get-docker.sh 7 | # 8 | # For test builds (ie. release candidates): 9 | # $ curl -fsSL https://test.docker.com -o test-docker.sh 10 | # $ sh test-docker.sh 11 | # 12 | # NOTE: Make sure to verify the contents of the script 13 | # you downloaded matches the contents of install.sh 14 | # located at https://github.com/docker/docker-install 15 | # before executing. 16 | # 17 | # Git commit from https://github.com/docker/docker-install when 18 | # the script was uploaded (Should only be modified by upload job): 19 | SCRIPT_COMMIT_SHA=2f4ae48 20 | 21 | # The channel to install from: 22 | # * nightly 23 | # * test 24 | # * stable 25 | # * edge (deprecated) 26 | DEFAULT_CHANNEL_VALUE="stable" 27 | if [ -z "$CHANNEL" ]; then 28 | CHANNEL=$DEFAULT_CHANNEL_VALUE 29 | fi 30 | 31 | DEFAULT_DOWNLOAD_URL="https://download.docker.com" 32 | if [ -z "$DOWNLOAD_URL" ]; then 33 | DOWNLOAD_URL=$DEFAULT_DOWNLOAD_URL 34 | fi 35 | 36 | DEFAULT_REPO_FILE="docker-ce.repo" 37 | if [ -z "$REPO_FILE" ]; then 38 | REPO_FILE="$DEFAULT_REPO_FILE" 39 | fi 40 | 41 | mirror='' 42 | DRY_RUN=${DRY_RUN:-} 43 | while [ $# -gt 0 ]; do 44 | case "$1" in 45 | --mirror) 46 | mirror="$2" 47 | shift 48 | ;; 49 | --dry-run) 50 | DRY_RUN=1 51 | ;; 52 | --*) 53 | echo "Illegal option $1" 54 | ;; 55 | esac 56 | shift $(($# > 0 ? 1 : 0)) 57 | done 58 | 59 | case "$mirror" in 60 | Aliyun) 61 | DOWNLOAD_URL="https://mirrors.aliyun.com/docker-ce" 62 | ;; 63 | AzureChinaCloud) 64 | DOWNLOAD_URL="https://mirror.azure.cn/docker-ce" 65 | ;; 66 | esac 67 | 68 | command_exists() { 69 | command -v "$@" >/dev/null 2>&1 70 | } 71 | 72 | is_dry_run() { 73 | if [ -z "$DRY_RUN" ]; then 74 | return 1 75 | else 76 | return 0 77 | fi 78 | } 79 | 80 | deprecation_notice() { 81 | distro=$1 82 | date=$2 83 | echo 84 | echo "DEPRECATION WARNING:" 85 | echo " The distribution, $distro, will no longer be supported in this script as of $date." 86 | echo " If you feel this is a mistake please submit an issue at https://github.com/docker/docker-install/issues/new" 87 | echo 88 | sleep 10 89 | } 90 | 91 | get_distribution() { 92 | lsb_dist="" 93 | # Every system that we officially support has /etc/os-release 94 | if [ -r /etc/os-release ]; then 95 | lsb_dist="$(. /etc/os-release && echo "$ID")" 96 | fi 97 | # Returning an empty string here should be alright since the 98 | # case statements don't act unless you provide an actual value 99 | echo "$lsb_dist" 100 | } 101 | 102 | add_debian_backport_repo() { 103 | debian_version="$1" 104 | backports="deb http://ftp.debian.org/debian $debian_version-backports main" 105 | if ! grep -Fxq "$backports" /etc/apt/sources.list; then 106 | ( 107 | set -x 108 | $sh_c "echo \"$backports\" >> /etc/apt/sources.list" 109 | ) 110 | fi 111 | } 112 | 113 | echo_docker_as_nonroot() { 114 | if is_dry_run; then 115 | return 116 | fi 117 | if command_exists docker && [ -e /var/run/docker.sock ]; then 118 | ( 119 | set -x 120 | $sh_c 'docker version' 121 | ) || true 122 | fi 123 | your_user=your-user 124 | [ "$user" != 'root' ] && your_user="$user" 125 | # intentionally mixed spaces and tabs here -- tabs are stripped by "<<-EOF", spaces are kept in the output 126 | echo "If you would like to use Docker as a non-root user, you should now consider" 127 | echo "adding your user to the \"docker\" group with something like:" 128 | echo 129 | echo " sudo usermod -aG docker $your_user" 130 | echo 131 | echo "Remember that you will have to log out and back in for this to take effect!" 132 | echo 133 | echo "WARNING: Adding a user to the \"docker\" group will grant the ability to run" 134 | echo " containers which can be used to obtain root privileges on the" 135 | echo " docker host." 136 | echo " Refer to https://docs.docker.com/engine/security/security/#docker-daemon-attack-surface" 137 | echo " for more information." 138 | 139 | } 140 | 141 | # Check if this is a forked Linux distro 142 | check_forked() { 143 | 144 | # Check for lsb_release command existence, it usually exists in forked distros 145 | if command_exists lsb_release; then 146 | # Check if the `-u` option is supported 147 | set +e 148 | lsb_release -a -u >/dev/null 2>&1 149 | lsb_release_exit_code=$? 150 | set -e 151 | 152 | # Check if the command has exited successfully, it means we're in a forked distro 153 | if [ "$lsb_release_exit_code" = "0" ]; then 154 | # Print info about current distro 155 | cat <<-EOF 156 | You're using '$lsb_dist' version '$dist_version'. 157 | EOF 158 | 159 | # Get the upstream release info 160 | lsb_dist=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'id' | cut -d ':' -f 2 | tr -d '[:space:]') 161 | dist_version=$(lsb_release -a -u 2>&1 | tr '[:upper:]' '[:lower:]' | grep -E 'codename' | cut -d ':' -f 2 | tr -d '[:space:]') 162 | 163 | # Print info about upstream distro 164 | cat <<-EOF 165 | Upstream release is '$lsb_dist' version '$dist_version'. 166 | EOF 167 | else 168 | if [ -r /etc/debian_version ] && [ "$lsb_dist" != "ubuntu" ] && [ "$lsb_dist" != "raspbian" ]; then 169 | if [ "$lsb_dist" = "osmc" ]; then 170 | # OSMC runs Raspbian 171 | lsb_dist=raspbian 172 | else 173 | # We're Debian and don't even know it! 174 | lsb_dist=debian 175 | fi 176 | dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" 177 | case "$dist_version" in 178 | 9) 179 | dist_version="stretch" 180 | ;; 181 | 8 | 'Kali Linux 2') 182 | dist_version="jessie" 183 | ;; 184 | esac 185 | fi 186 | fi 187 | fi 188 | } 189 | 190 | semverParse() { 191 | major="${1%%.*}" 192 | minor="${1#$major.}" 193 | minor="${minor%%.*}" 194 | patch="${1#$major.$minor.}" 195 | patch="${patch%%[-.]*}" 196 | } 197 | 198 | ee_notice() { 199 | echo 200 | echo 201 | echo " WARNING: $1 is now only supported by Docker EE" 202 | echo " Check https://store.docker.com for information on Docker EE" 203 | echo 204 | echo 205 | } 206 | 207 | do_install() { 208 | echo "# Executing docker install script, commit: $SCRIPT_COMMIT_SHA" 209 | 210 | if command_exists docker; then 211 | docker_version="$(docker -v | cut -d ' ' -f3 | cut -d ',' -f1)" 212 | MAJOR_W=1 213 | MINOR_W=10 214 | 215 | semverParse "$docker_version" 216 | 217 | shouldWarn=0 218 | if [ "$major" -lt "$MAJOR_W" ]; then 219 | shouldWarn=1 220 | fi 221 | 222 | if [ "$major" -le "$MAJOR_W" ] && [ "$minor" -lt "$MINOR_W" ]; then 223 | shouldWarn=1 224 | fi 225 | 226 | cat >&2 <<-'EOF' 227 | Warning: the "docker" command appears to already exist on this system. 228 | 229 | If you already have Docker installed, this script can cause trouble, which is 230 | why we're displaying this warning and provide the opportunity to cancel the 231 | installation. 232 | 233 | If you installed the current Docker package using this script and are using it 234 | EOF 235 | 236 | if [ $shouldWarn -eq 1 ]; then 237 | cat >&2 <<-'EOF' 238 | again to update Docker, we urge you to migrate your image store before upgrading 239 | to v1.10+. 240 | 241 | You can find instructions for this here: 242 | https://github.com/docker/docker/wiki/Engine-v1.10.0-content-addressability-migration 243 | EOF 244 | else 245 | cat >&2 <<-'EOF' 246 | again to update Docker, you can safely ignore this message. 247 | EOF 248 | fi 249 | 250 | cat >&2 <<-'EOF' 251 | 252 | You may press Ctrl+C now to abort this script. 253 | EOF 254 | ( 255 | set -x 256 | sleep 20 257 | ) 258 | fi 259 | 260 | user="$(id -un 2>/dev/null || true)" 261 | 262 | sh_c='sh -c' 263 | if [ "$user" != 'root' ]; then 264 | if command_exists sudo; then 265 | sh_c='sudo -E sh -c' 266 | elif command_exists su; then 267 | sh_c='su -c' 268 | else 269 | cat >&2 <<-'EOF' 270 | Error: this installer needs the ability to run commands as root. 271 | We are unable to find either "sudo" or "su" available to make this happen. 272 | EOF 273 | exit 1 274 | fi 275 | fi 276 | 277 | if is_dry_run; then 278 | sh_c="echo" 279 | fi 280 | 281 | # perform some very rudimentary platform detection 282 | lsb_dist=$(get_distribution) 283 | lsb_dist="$(echo "$lsb_dist" | tr '[:upper:]' '[:lower:]')" 284 | 285 | case "$lsb_dist" in 286 | 287 | ubuntu) 288 | if command_exists lsb_release; then 289 | dist_version="$(lsb_release --codename | cut -f2)" 290 | fi 291 | if [ -z "$dist_version" ] && [ -r /etc/lsb-release ]; then 292 | dist_version="$(. /etc/lsb-release && echo "$DISTRIB_CODENAME")" 293 | fi 294 | ;; 295 | 296 | debian | raspbian) 297 | dist_version="$(sed 's/\/.*//' /etc/debian_version | sed 's/\..*//')" 298 | case "$dist_version" in 299 | 9) 300 | dist_version="stretch" 301 | ;; 302 | 8) 303 | dist_version="jessie" 304 | ;; 305 | esac 306 | ;; 307 | 308 | centos) 309 | if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then 310 | dist_version="$(. /etc/os-release && echo "$VERSION_ID")" 311 | fi 312 | ;; 313 | 314 | rhel | ol | sles) 315 | ee_notice "$lsb_dist" 316 | exit 1 317 | ;; 318 | 319 | *) 320 | if command_exists lsb_release; then 321 | dist_version="$(lsb_release --release | cut -f2)" 322 | fi 323 | if [ -z "$dist_version" ] && [ -r /etc/os-release ]; then 324 | dist_version="$(. /etc/os-release && echo "$VERSION_ID")" 325 | fi 326 | ;; 327 | 328 | esac 329 | 330 | # Check if this is a forked Linux distro 331 | check_forked 332 | 333 | # Run setup for each distro accordingly 334 | case "$lsb_dist" in 335 | ubuntu | debian | raspbian) 336 | pre_reqs="apt-transport-https ca-certificates curl" 337 | if [ "$lsb_dist" = "debian" ]; then 338 | # libseccomp2 does not exist for debian jessie main repos for aarch64 339 | if [ "$(uname -m)" = "aarch64" ] && [ "$dist_version" = "jessie" ]; then 340 | add_debian_backport_repo "$dist_version" 341 | fi 342 | fi 343 | 344 | if ! command -v gpg >/dev/null; then 345 | pre_reqs="$pre_reqs gnupg" 346 | fi 347 | apt_repo="deb [arch=$(dpkg --print-architecture)] $DOWNLOAD_URL/linux/$lsb_dist $dist_version $CHANNEL" 348 | ( 349 | if ! is_dry_run; then 350 | set -x 351 | fi 352 | $sh_c 'apt-get update -qq >/dev/null' 353 | $sh_c "apt-get install -y -qq $pre_reqs >/dev/null" 354 | $sh_c "curl -fsSL \"$DOWNLOAD_URL/linux/$lsb_dist/gpg\" | apt-key add -qq - >/dev/null" 355 | $sh_c "echo \"$apt_repo\" > /etc/apt/sources.list.d/docker.list" 356 | $sh_c 'apt-get update -qq >/dev/null' 357 | ) 358 | pkg_version="" 359 | if [ -n "$VERSION" ]; then 360 | if is_dry_run; then 361 | echo "# WARNING: VERSION pinning is not supported in DRY_RUN" 362 | else 363 | # Will work for incomplete versions IE (17.12), but may not actually grab the "latest" if in the test channel 364 | pkg_pattern="$(echo "$VERSION" | sed "s/-ce-/~ce~.*/g" | sed "s/-/.*/g").*-0~$lsb_dist" 365 | search_command="apt-cache madison 'docker-ce' | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3" 366 | pkg_version="$($sh_c "$search_command")" 367 | echo "INFO: Searching repository for VERSION '$VERSION'" 368 | echo "INFO: $search_command" 369 | if [ -z "$pkg_version" ]; then 370 | echo 371 | echo "ERROR: '$VERSION' not found amongst apt-cache madison results" 372 | echo 373 | exit 1 374 | fi 375 | search_command="apt-cache madison 'docker-ce-cli' | grep '$pkg_pattern' | head -1 | awk '{\$1=\$1};1' | cut -d' ' -f 3" 376 | # Don't insert an = for cli_pkg_version, we'll just include it later 377 | cli_pkg_version="$($sh_c "$search_command")" 378 | pkg_version="=$pkg_version" 379 | fi 380 | fi 381 | ( 382 | if ! is_dry_run; then 383 | set -x 384 | fi 385 | if [ -n "$cli_pkg_version" ]; then 386 | $sh_c "apt-get install -y -qq --no-install-recommends docker-ce-cli=$cli_pkg_version >/dev/null" 387 | fi 388 | $sh_c "apt-get install -y -qq --no-install-recommends docker-ce$pkg_version >/dev/null" 389 | ) 390 | echo_docker_as_nonroot 391 | exit 0 392 | ;; 393 | centos | fedora) 394 | yum_repo="$DOWNLOAD_URL/linux/$lsb_dist/$REPO_FILE" 395 | if ! curl -Ifs "$yum_repo" >/dev/null; then 396 | echo "Error: Unable to curl repository file $yum_repo, is it valid?" 397 | exit 1 398 | fi 399 | if [ "$lsb_dist" = "fedora" ]; then 400 | pkg_manager="dnf" 401 | config_manager="dnf config-manager" 402 | enable_channel_flag="--set-enabled" 403 | disable_channel_flag="--set-disabled" 404 | pre_reqs="dnf-plugins-core" 405 | pkg_suffix="fc$dist_version" 406 | else 407 | pkg_manager="yum" 408 | config_manager="yum-config-manager" 409 | enable_channel_flag="--enable" 410 | disable_channel_flag="--disable" 411 | pre_reqs="yum-utils" 412 | pkg_suffix="el" 413 | fi 414 | ( 415 | if ! is_dry_run; then 416 | set -x 417 | fi 418 | $sh_c "$pkg_manager install -y -q $pre_reqs" 419 | $sh_c "$config_manager --add-repo $yum_repo" 420 | 421 | if [ "$CHANNEL" != "stable" ]; then 422 | $sh_c "$config_manager $disable_channel_flag docker-ce-*" 423 | $sh_c "$config_manager $enable_channel_flag docker-ce-$CHANNEL" 424 | fi 425 | $sh_c "$pkg_manager makecache" 426 | ) 427 | pkg_version="" 428 | if [ -n "$VERSION" ]; then 429 | if is_dry_run; then 430 | echo "# WARNING: VERSION pinning is not supported in DRY_RUN" 431 | else 432 | pkg_pattern="$(echo "$VERSION" | sed "s/-ce-/\\\\.ce.*/g" | sed "s/-/.*/g").*$pkg_suffix" 433 | search_command="$pkg_manager list --showduplicates 'docker-ce' | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'" 434 | pkg_version="$($sh_c "$search_command")" 435 | echo "INFO: Searching repository for VERSION '$VERSION'" 436 | echo "INFO: $search_command" 437 | if [ -z "$pkg_version" ]; then 438 | echo 439 | echo "ERROR: '$VERSION' not found amongst $pkg_manager list results" 440 | echo 441 | exit 1 442 | fi 443 | search_command="$pkg_manager list --showduplicates 'docker-ce-cli' | grep '$pkg_pattern' | tail -1 | awk '{print \$2}'" 444 | # It's okay for cli_pkg_version to be blank, since older versions don't support a cli package 445 | cli_pkg_version="$($sh_c "$search_command" | cut -d':' -f 2)" 446 | # Cut out the epoch and prefix with a '-' 447 | pkg_version="-$(echo "$pkg_version" | cut -d':' -f 2)" 448 | fi 449 | fi 450 | ( 451 | if ! is_dry_run; then 452 | set -x 453 | fi 454 | # install the correct cli version first 455 | if [ -n "$cli_pkg_version" ]; then 456 | $sh_c "$pkg_manager install -y -q docker-ce-cli-$cli_pkg_version" 457 | fi 458 | $sh_c "$pkg_manager install -y -q docker-ce$pkg_version" 459 | ) 460 | echo_docker_as_nonroot 461 | exit 0 462 | ;; 463 | *) 464 | echo 465 | echo "ERROR: Unsupported distribution '$lsb_dist'" 466 | echo 467 | exit 1 468 | ;; 469 | esac 470 | exit 1 471 | } 472 | 473 | # wrapped up in a function so that we have some protection against only getting 474 | # half the file during "curl | sh" 475 | do_install 476 | -------------------------------------------------------------------------------- /provision/images/node-builder-10-alpine/Dockerfile: -------------------------------------------------------------------------------- 1 | # syntax = docker/dockerfile:experimental 2 | # ===== Builder ===== 3 | # =================== 4 | FROM node:10-alpine AS builder 5 | 6 | RUN apk --no-cache add \ 7 | g++ make python git \ 8 | && yarn global add node-gyp \ 9 | && rm -rf /var/cache/apk/* 10 | -------------------------------------------------------------------------------- /provision/setup-docker.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Install docker 3 | curl -fsSL get.docker.com -o get-docker.sh 4 | sudo sh get-docker.sh 5 | docker -v 6 | 7 | # Install docker-compose 8 | sudo curl -L https://github.com/docker/compose/releases/download/1.22.0/docker-compose-$(uname -s)-$(uname -m) -o /usr/local/bin/docker-compose 9 | sudo chmod +x /usr/local/bin/docker-compose 10 | docker-compose -v 11 | 12 | sudo sed -i '/^ExecStart.*$/c\ExecStart=/usr/bin/dockerd -H unix:// -H tcp://0.0.0.0:6513' /lib/systemd/system/docker.service 13 | # Restart docker daemon 14 | sudo systemctl daemon-reload 15 | sudo systemctl restart docker 16 | -------------------------------------------------------------------------------- /provision/setup.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | echo 'DOCKER VERSION' 3 | docker -v 4 | 5 | echo 'DOCKER COMPOSE VERSION' 6 | docker-compose -v 7 | 8 | echo 'CREATE DATA DIRECTORY' 9 | mkdir -p ./data 10 | 11 | echo 'COMPOSE UP' 12 | docker-compose --project-name=ltv up -d 13 | --------------------------------------------------------------------------------