diff --git a/.env.example b/.env.example new file mode 100644 index 0000000..1f8b8ec --- /dev/null +++ b/.env.example @@ -0,0 +1,2 @@ +DATABASE_URL=your-database-url-should-be-written-here +SESSION_SECRET=change_this_to_any_long_random_string_with_no_spaces2348fklvjcxbkjw3q3slo \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js new file mode 100644 index 0000000..309645a --- /dev/null +++ b/.eslintrc.js @@ -0,0 +1,22 @@ +module.exports = { + env: { + browser: false, + node: true, + commonjs: true, + es2021: true, + jest: true, + }, + extends: "eslint:recommended", + overrides: [ + { + files: [".eslintrc.{js,cjs}"], + parserOptions: { + sourceType: "script", + }, + }, + ], + parserOptions: { + ecmaVersion: "latest", + }, + rules: {}, +}; diff --git a/.github/workflows/ci-for-node.yaml b/.github/workflows/ci-for-node.yaml new file mode 100644 index 0000000..8fe2cb1 --- /dev/null +++ b/.github/workflows/ci-for-node.yaml @@ -0,0 +1,33 @@ +# This workflow will do a clean installation of node dependencies, cache/restore them, build the source code and run tests across different versions of node +# For more information see: https://docs.github.com/en/actions/automating-builds-and-tests/building-and-testing-nodejs + +name: Node.js CI + +on: + push: + branches: ["main"] + pull_request: + branches: ["main"] + +jobs: + build: + runs-on: ubuntu-latest + + strategy: + matrix: + node-version: [20.x] + # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + cache: "yarn" + - name: install dependencies with yarn + run: yarn install --frozen-lockfile + - run: yarn test + run: yarn lint + - name: Check code formatting with prettier + run: yarn format:check diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..0947dac --- /dev/null +++ b/.gitignore @@ -0,0 +1,132 @@ +.DS_Store + +# Logs +logs +*.log +npm-debug.log* +yarn-debug.log* +yarn-error.log* +lerna-debug.log* +.pnpm-debug.log* + +# Diagnostic reports (https://nodejs.org/api/report.html) +report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json + +# Runtime data +pids +*.pid +*.seed +*.pid.lock + +# Directory for instrumented libs generated by jscoverage/JSCover +lib-cov + +# Coverage directory used by tools like istanbul +coverage +*.lcov + +# nyc test coverage +.nyc_output + +# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) +.grunt + +# Bower dependency directory (https://bower.io/) +bower_components + +# node-waf configuration +.lock-wscript + +# Compiled binary addons (https://nodejs.org/api/addons.html) +build/Release + +# Dependency directories +node_modules/ +jspm_packages/ + +# Snowpack dependency directory (https://snowpack.dev/) +web_modules/ + +# TypeScript cache +*.tsbuildinfo + +# Optional npm cache directory +.npm + +# Optional eslint cache +.eslintcache + +# Optional stylelint cache +.stylelintcache + +# Microbundle cache +.rpt2_cache/ +.rts2_cache_cjs/ +.rts2_cache_es/ +.rts2_cache_umd/ + +# Optional REPL history +.node_repl_history + +# Output of 'npm pack' +*.tgz + +# Yarn Integrity file +.yarn-integrity + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# parcel-bundler cache (https://parceljs.org/) +.cache +.parcel-cache + +# Next.js build output +.next +out + +# Nuxt.js build / generate output +.nuxt +dist + +# Gatsby files +.cache/ +# Comment in the public line in if your project uses Gatsby and not Next.js +# https://nextjs.org/blog/next-9-1#public-directory-support +# public + +# vuepress build output +.vuepress/dist + +# vuepress v2.x temp and cache directory +.temp +.cache + +# Docusaurus cache and generated files +.docusaurus + +# Serverless directories +.serverless/ + +# FuseBox cache +.fusebox/ + +# DynamoDB Local files +.dynamodb/ + +# TernJS port file +.tern-port + +# Stores VSCode versions used for testing VSCode extensions +.vscode-test + +# yarn v2 +.yarn/cache +.yarn/unplugged +.yarn/build-state.yml +.yarn/install-state.gz +.pnp.* diff --git a/.node_version b/.node_version new file mode 100644 index 0000000..209e3ef --- /dev/null +++ b/.node_version @@ -0,0 +1 @@ +20 diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 0000000..451c0ab --- /dev/null +++ b/.prettierignore @@ -0,0 +1,13 @@ +# Ignore artifacts: +build +coverage +dist + +yarn.lock + +**/*.log + +# (Example) Tell prettier to ignore all HTML files: +# **/*.html + +# version control and node_modules are ignored by default \ No newline at end of file diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..0a02bce --- /dev/null +++ b/.prettierrc @@ -0,0 +1,3 @@ +{ + "tabWidth": 4 +} diff --git a/README.md b/README.md new file mode 100644 index 0000000..7cc2a0e --- /dev/null +++ b/README.md @@ -0,0 +1,87 @@ +# Starter code for the academy tv-shows express web app project + +This is an intermediate starter template for express apps written in JavaScript (not TypeScript). + +It does not use express router - you can add that if you need it. + +## Change this README.md file! + +If you have used this project as a template, remember to change this readme file to add your own documentation and remove anything you don't need. + +## Features + +- EJS template setup with express +- Database support: + + - connection-pool setup for [node-postgres](https://node-postgres.com/) + - loads DATABASE_URL from env variable. (And tries to load `.env` files with `dotenv`) + +- live-reload +- automated testing with jest + - example jest test +- formatting with prettier +- linting with eslint +- workflow config for CI on github (see [.github/workflows/ci-for-node.yaml](.github/workflows/ci-for-node.yaml) ) +- jsconfig.json (to enable vscode error-reporting in type-checked js files) +- express-session (only with flaky in-memory support. not meant for production use, only development) +- .gitignore +- logging with [morgan](https://expressjs.com/en/resources/middleware/morgan.html) +- demo of classless css framework ([sakura.css](https://oxal.org/projects/sakura/) or mvp.css, etc) +- static file serving from `/public` + - favicon +- TODO: error-handling + +### note about live reload + +In development mode, when viewing an html page, if a file is changed and saved, the browser will make the request again. This won't work if the browser is viewing a json output - it needs to be an html page so that live-reload can insert a javascript fragment into it. + +## Installation + +Install dependencies + +`yarn` + +## Configuration on dev machine + +Copy `.env.example` to `.env` and set any variables there appropriately: + +- `DATABASE_URL` to your database's connection string. +- `SESSION_SECRET` to a long very random string with no spaces, unique to your application. + +If your express app isn't going to need sessions, you can remove the session-setup code from the express setup (probably in `setupExpress.js`) and this will remove the need for the related environment variable. + +## Running + +Before you change any code (with exception of creating a suitable `.env` file), check that the app runs and handles a request to `/`. + +(It's ok if your database doesn't exist yet, provided you don't make request to `/db-test`) + +Run (in dev mode with live-reload): + +`yarn start:dev` + +Run (for production with no live-reload) + +`yarn start` + +## Running tests + +`yarn test` + +## Linting + +`yarn lint` + +## Formatting with prettier + +`yarn format` + +However, it is suggested you install vscode's prettier extension and enable the user setting `format on save`. When formatting, VSCode will notice the .prettierrc and format according to those rules (and prettier's defaults). + +## CI (linting, formatting check, automated tests) + +This project includes a workflow file in [.github](.github) which will cause CI to run on github. + +## Debugging with vscode + +Set a breakpoint in the margin of any JS file, and use run-and-debug (ctrl-shift-d)'s `Run and Debug` button to start express. Bear in mind that a breakpoint set in a request handler won't cause express to pause until a matching request comes in! diff --git a/jsconfig.json b/jsconfig.json new file mode 100644 index 0000000..6eb7e62 --- /dev/null +++ b/jsconfig.json @@ -0,0 +1,9 @@ +{ + "compilerOptions": { + "module": "commonjs", + "target": "esnext", + "checkJs": true, + }, + "exclude": ["node_modules", "**/node_modules/*"], + "include": ["src/**/*"], +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..264a208 --- /dev/null +++ b/package.json @@ -0,0 +1,33 @@ +{ + "name": "academy-express-tv-shows-project-starter", + "version": "1.0.0", + "main": "src/app.js", + "author": "nbogie", + "license": "MIT", + "scripts": { + "start": "node src/app.js", + "start:dev": "nodemon src/app.js --ext 'js ejs html json css'", + "test": "jest --verbose", + "lint": "yarn eslint .", + "format": "yarn prettier --write .", + "format:check": "yarn prettier ." + }, + "dependencies": { + "cors": "^2.8.5", + "dotenv": "^16.4.1", + "ejs": "^3.1.9", + "express": "^4.18.2", + "express-session": "^1.17.3", + "morgan": "^1.10.0", + "pg": "^8.11.3" + }, + "devDependencies": { + "@types/jest": "^29.5.11", + "connect-livereload": "^0.6.1", + "eslint": "^8.56.0", + "jest": "^29.7.0", + "livereload": "^0.9.3", + "nodemon": "^3.0.3", + "prettier": "^3.2.4" + } +} diff --git a/public/favicon.png b/public/favicon.png new file mode 100644 index 0000000..62f6c87 Binary files /dev/null and b/public/favicon.png differ diff --git a/public/style.css b/public/style.css new file mode 100644 index 0000000..06f307c --- /dev/null +++ b/public/style.css @@ -0,0 +1,14 @@ +* { + box-sizing: border-box; +} + +body { + margin: 0 auto; + max-width: 1200px; + /* height: 100vh; */ +} + +.product { + display: grid; + grid-template-columns: 3fr 0.4fr 0.4fr 0.4fr 1fr 2fr; +} diff --git a/src/app.js b/src/app.js new file mode 100644 index 0000000..1f72c20 --- /dev/null +++ b/src/app.js @@ -0,0 +1,31 @@ +const { app } = require("./support/setupExpress"); +const { query } = require("./support/db"); + +//configure the server's route handlers +app.get("/", (req, res) => { + res.render("pages/index"); +}); + +app.get("/db-test", async (req, res) => { + try { + const dbResult = await query("select now()"); + const rows = dbResult.rows; + res.json(rows); + } catch (err) { + console.error(err); + res.status(500).send( + "Sorry, an error occurred on the server. Ask the dev team to check the server logs at time " + + new Date(), + ); + } +}); + +// use the environment variable PORT, or 3000 as a fallback if it is undefined +const PORT_NUMBER = process.env.PORT ?? 3000; + +//start the server listening indefinitely +app.listen(PORT_NUMBER, () => { + console.log( + `Your express app started listening on ${PORT_NUMBER} at ${new Date()}`, + ); +}); diff --git a/src/dice.js b/src/dice.js new file mode 100644 index 0000000..4c9fd42 --- /dev/null +++ b/src/dice.js @@ -0,0 +1,10 @@ +/** + * Returns a random d6 die roll from 1-6. + * @returns {number} a number between 1 and 6 inclusive, representing a d6 die roll. + */ +function randomDieRoll() { + const n = 1 + Math.floor(Math.random() * 6); + return n; +} + +exports.randomDieRoll = randomDieRoll; diff --git a/src/dice.test.js b/src/dice.test.js new file mode 100644 index 0000000..9a1b851 --- /dev/null +++ b/src/dice.test.js @@ -0,0 +1,7 @@ +const { randomDieRoll } = require("./dice"); + +test("randomDieRoll", () => { + for (let i = 0; i < 1000; i++) { + expect([1, 2, 3, 4, 5, 6]).toContain(randomDieRoll()); + } +}); diff --git a/src/support/db.js b/src/support/db.js new file mode 100644 index 0000000..807bf64 --- /dev/null +++ b/src/support/db.js @@ -0,0 +1,32 @@ +const { Pool } = require("pg"); +const { getEnvVarOrFail } = require("./envVarHelp"); + +/** + * A small pool of connections to the database specified in the env var `DATABASE_URL` + * @see https://node-postgres.com/apis/pool + */ +const pool = new Pool({ + connectionString: getEnvVarOrFail("DATABASE_URL"), + max: 2, //keep this low. elephantSQL doesn't let you have a lot of connections for free. +}); + +/** + * Promises to execute the given SQL query, optionally using any given values in place of placeholders $1, $2, etc. + * + * @param {string} sql The SQL query to execute + * @param {any[]} values + * + * Note: Returns a promise - you'll need to `await` its resolution, or schedule a subsequent function with `.then()` + * + * Note: Don't use this for multi-query transactions - see https://node-postgres.com/apis/pool#poolquery + + */ +async function query(sql, values = []) { + console.log("running sql: ", sql); + const dbResult = await pool.query(sql, values); + console.log( + `Queried db and got : ${dbResult.rowCount} row(s). SQL was: ${sql}`, + ); + return dbResult; +} +module.exports = { pool, query }; diff --git a/src/support/envVarHelp.js b/src/support/envVarHelp.js new file mode 100644 index 0000000..3f1505f --- /dev/null +++ b/src/support/envVarHelp.js @@ -0,0 +1,14 @@ +/** + * Returns the value of the given environment variable, or throws an error if it does not exist. + * @param {string} envVarKey key of environment variable to obtain + */ +function getEnvVarOrFail(envVarKey) { + const foundValue = process.env[envVarKey]; + if (!foundValue) { + throw new Error( + `Missing expected env var ${envVarKey}. Have you set it in an .env file or via host UI?`, + ); + } + return foundValue; +} +module.exports = { getEnvVarOrFail }; diff --git a/src/support/liveReloadSupport.js b/src/support/liveReloadSupport.js new file mode 100644 index 0000000..a9ffdf7 --- /dev/null +++ b/src/support/liveReloadSupport.js @@ -0,0 +1,12 @@ +const livereload = require("livereload"); +const connectLiveReload = require("connect-livereload"); +const liveReloadServer = livereload.createServer({ + // debug: true +}); +liveReloadServer.server.once("connection", () => { + setTimeout(() => { + liveReloadServer.refresh("/"); + }, 100); +}); + +module.exports = { connectLiveReload }; diff --git a/src/support/setupExpress.js b/src/support/setupExpress.js new file mode 100644 index 0000000..b7304fe --- /dev/null +++ b/src/support/setupExpress.js @@ -0,0 +1,36 @@ +const express = require("express"); +const { connectLiveReload } = require("./liveReloadSupport"); +require("dotenv").config(); //load key-value pairs from any .env files into process.env +const cors = require("cors"); +const session = require("express-session"); +const { getEnvVarOrFail } = require("./envVarHelp"); +const morgan = require("morgan"); + +const app = express(); + +//any requests for files which are found in public will be served. e.g. /index.html will serve from /oublic/index.html +app.use(express.static("public")); +app.use(morgan("dev")); +app.set("view engine", "ejs"); + +//parse any form content from request body (application/x-www-form-urlencoded), making available as req.body +app.use(express.urlencoded({ extended: false })); + +//auto-include CORS headers to allow consumption of our content by in-browser js loaded from elsewhere +app.use(cors()); + +app.use( + session({ + secret: getEnvVarOrFail("SESSION_SECRET"), + resave: false, + saveUninitialized: false, + }), +); + +console.log("process.env.NODE_ENV is " + process.env.NODE_ENV); +if (process.env.NODE_ENV !== "production") { + console.log("Enabling live-reloading of html pages on file save."); + app.use(connectLiveReload()); +} + +module.exports = { app }; diff --git a/src/types/index.ts b/src/types/index.ts new file mode 100644 index 0000000..943bf2a --- /dev/null +++ b/src/types/index.ts @@ -0,0 +1,9 @@ +import "express-session"; + +//This extension to the SessionData object will help vscode with the types of the session object +//Add whatever key-value pairs you want to the SessionData interface +declare module "express-session" { + export interface SessionData { + messages: string[]; + } +} diff --git a/views/pages/index.ejs b/views/pages/index.ejs new file mode 100644 index 0000000..0624e0d --- /dev/null +++ b/views/pages/index.ejs @@ -0,0 +1,9 @@ +<%- include("../partials/header.ejs", {title: "Main Page"}) %> + +
This is the main page, index.ejs
+ +this request will test that the db connection is ok + +