From 720b55eeebbf5630159536771210ddb9baede6be Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Tue, 30 Jan 2024 09:46:06 +0000 Subject: [PATCH 01/10] Add Postgres --- .eslintrc | 9 - .eslintrc.json | 16 ++ .github/workflows/push.yml | 14 ++ api/{.eslintrc => .eslintrc.json} | 5 +- api/app.js | 10 +- api/db.js | 26 +++ api/jest.config.js | 1 + api/package.json | 1 + api/server.js | 8 +- api/setupTests.js | 5 + api/utils/config.js | 14 ++ api/utils/middleware.js | 11 + e2e/{.eslintrc => .eslintrc.json} | 0 package-lock.json | 326 ++++++++++++++++++++++++++++++ package.json | 1 + web/{.eslintrc => .eslintrc.json} | 0 web/vite.config.js | 2 +- 17 files changed, 435 insertions(+), 14 deletions(-) delete mode 100644 .eslintrc create mode 100644 .eslintrc.json rename api/{.eslintrc => .eslintrc.json} (86%) create mode 100644 api/db.js create mode 100644 api/setupTests.js rename e2e/{.eslintrc => .eslintrc.json} (100%) rename web/{.eslintrc => .eslintrc.json} (100%) diff --git a/.eslintrc b/.eslintrc deleted file mode 100644 index 10cf1b3f..00000000 --- a/.eslintrc +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": ["@codeyourfuture/standard", "prettier"], - "parserOptions": { - "ecmaVersion": 2021, - "sourceType": "module" - }, - "reportUnusedDisableDirectives": true, - "root": true -} diff --git a/.eslintrc.json b/.eslintrc.json new file mode 100644 index 00000000..6dd437c0 --- /dev/null +++ b/.eslintrc.json @@ -0,0 +1,16 @@ +{ + "extends": ["@codeyourfuture/standard", "prettier"], + "parserOptions": { + "ecmaVersion": 2022, + "sourceType": "module" + }, + "plugins": ["import"], + "reportUnusedDisableDirectives": true, + "root": true, + "rules": { + "import/order": [ + "error", + { "alphabetize": { "order": "asc" }, "newlines-between": "always" } + ] + } +} diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 68f9eefa..672e4608 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -9,6 +9,19 @@ on: jobs: nodejs: runs-on: ubuntu-latest + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: keepitsecret + POSTGRES_USER: testdb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - uses: textbook/take-action@nodejs with: @@ -17,6 +30,7 @@ jobs: - run: npm run lint - run: npm run test:cover env: + DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb FORCE_COLOR: true - run: npm run e2e - run: npm run e2e:dev diff --git a/api/.eslintrc b/api/.eslintrc.json similarity index 86% rename from api/.eslintrc rename to api/.eslintrc.json index eed8a20e..60c2b464 100644 --- a/api/.eslintrc +++ b/api/.eslintrc.json @@ -3,7 +3,7 @@ "ignorePatterns": ["/static/"], "overrides": [ { - "files": ["*.test.js"], + "files": ["*.test.js", "setupTests.js"], "extends": ["plugin:jest/recommended"], "rules": { "jest/expect-expect": [ @@ -14,6 +14,9 @@ } } ], + "parserOptions": { + "ecmaVersion": 2022 + }, "rules": { "no-console": "warn", "no-restricted-syntax": [ diff --git a/api/app.js b/api/app.js index fe1d95f8..31309f11 100644 --- a/api/app.js +++ b/api/app.js @@ -1,7 +1,9 @@ import express from "express"; +import db from "./db.js"; import config from "./utils/config.js"; import { + asyncHandler, clientRouter, configuredHelmet, configuredMorgan, @@ -20,7 +22,13 @@ if (config.production) { app.use(httpsOnly()); } -app.get("/healthz", (_, res) => res.sendStatus(200)); +app.get( + "/healthz", + asyncHandler(async (_, res) => { + await db.query("SELECT 1;"); + res.sendStatus(200); + }), +); app.use(clientRouter("/api")); diff --git a/api/db.js b/api/db.js new file mode 100644 index 00000000..e4e95385 --- /dev/null +++ b/api/db.js @@ -0,0 +1,26 @@ +import { default as pg } from "pg"; + +import config from "./utils/config.js"; +import logger from "./utils/logger.js"; + +const pool = new pg.Pool({ + connectionString: config.databaseUrl, + connectionTimeoutMillis: 5_000, +}); + +pool.on("error", (err) => logger.error("%O", err)); + +export const connect = async () => { + const client = await pool.connect(); + logger.info("connected to %s", client.database); + client.release(); +}; + +export const disconnect = async () => await pool.end(); + +export default { + query(...args) { + logger.debug("Postgres query: %O", args); + return pool.query.apply(pool, args); + }, +}; diff --git a/api/jest.config.js b/api/jest.config.js index d6258327..84e6f263 100644 --- a/api/jest.config.js +++ b/api/jest.config.js @@ -5,4 +5,5 @@ export default { coverageReporters: [["json", { file: "api.json" }], "text"], rootDir: ".", transform: {}, + setupFilesAfterEnv: ["/setupTests.js"], }; diff --git a/api/package.json b/api/package.json index dff0c3c7..c80753ed 100644 --- a/api/package.json +++ b/api/package.json @@ -17,6 +17,7 @@ "express": "^4.18.2", "helmet": "^7.1.0", "morgan": "^1.10.0", + "pg": "^8.11.3", "winston": "^3.11.0" }, "devDependencies": { diff --git a/api/server.js b/api/server.js index d3a55b85..77d5362e 100644 --- a/api/server.js +++ b/api/server.js @@ -1,8 +1,12 @@ import app from "./app.js"; - +import { connect } from "./db.js"; import config from "./utils/config.js"; import logger from "./utils/logger.js"; const { port } = config; -app.listen(port, () => logger.info(`listening on ${port}`)); +await connect(); + +const server = app.listen(port); + +server.on("listening", () => logger.info(`listening on ${port}`)); diff --git a/api/setupTests.js b/api/setupTests.js new file mode 100644 index 00000000..70a7274e --- /dev/null +++ b/api/setupTests.js @@ -0,0 +1,5 @@ +import { connect, disconnect } from "./db.js"; + +beforeAll(() => connect()); + +afterAll(() => disconnect()); diff --git a/api/utils/config.js b/api/utils/config.js index b06e0f75..31227426 100644 --- a/api/utils/config.js +++ b/api/utils/config.js @@ -11,15 +11,29 @@ const dotenvPath = resolve( configDotenv({ path: dotenvPath }); +requireArgs(["DATABASE_URL"]); + /** + * @property {string} databaseUrl * @property {string} dotenvPath * @property {string} logLevel * @property {number} port * @property {boolean} production */ export default { + databaseUrl: process.env.DATABASE_URL, dotenvPath, logLevel: process.env.LOG_LEVEL?.toLowerCase() ?? "info", port: parseInt(process.env.PORT ?? "3000", 10), production: process.env.NODE_ENV?.toLowerCase() === "production", }; + +function requireArgs(required) { + const missing = required.filter((variable) => !process.env[variable]); + if (missing.length > 0) { + process.exitCode = 1; + throw new Error( + `missing required environment variable(s): ${missing.join(", ")}`, + ); + } +} diff --git a/api/utils/middleware.js b/api/utils/middleware.js index cc118ec9..9f0a38e7 100644 --- a/api/utils/middleware.js +++ b/api/utils/middleware.js @@ -9,6 +9,17 @@ import logger from "./logger.js"; const __dirname = dirname(fileURLToPath(import.meta.url)); +export const asyncHandler = (handler) => { + /** @type {import("express").RequestHandler} */ + return async (req, res, next) => { + try { + await handler(req, res, next); + } catch (err) { + next(err); + } + }; +}; + export const clientRouter = (apiRoot) => { const staticDir = join(__dirname, "..", "static"); const router = Router(); diff --git a/e2e/.eslintrc b/e2e/.eslintrc.json similarity index 100% rename from e2e/.eslintrc rename to e2e/.eslintrc.json diff --git a/package-lock.json b/package-lock.json index 7485fa1c..422faab6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ "cross-env": "^7.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", "husky": "^9.0.6", "lint-staged": "^15.2.0", "nyc": "^15.1.0", @@ -39,6 +40,7 @@ "express": "^4.18.2", "helmet": "^7.1.0", "morgan": "^1.10.0", + "pg": "^8.11.3", "winston": "^3.11.0" }, "devDependencies": { @@ -2577,6 +2579,12 @@ "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", "dev": true }, + "node_modules/@types/json5": { + "version": "0.0.29", + "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", + "integrity": "sha512-dRLjCWHYg4oaA77cxO64oO+7JwCwnIzkZPdrrC71jQmQtlhM556pwKo5bUzqvZndkVbeFLIIi+9TC40JNF5hNQ==", + "dev": true + }, "node_modules/@types/node": { "version": "20.11.10", "resolved": "https://registry.npmjs.org/@types/node/-/node-20.11.10.tgz", @@ -3281,6 +3289,25 @@ "node": ">=8" } }, + "node_modules/array.prototype.findlastindex": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/array.prototype.findlastindex/-/array.prototype.findlastindex-1.2.3.tgz", + "integrity": "sha512-LzLoiOMAxvy+Gd3BAq3B7VeIgPdo+Q8hthvKtXybMvRV0jrXfJM/t8mw7nNlpEcVlVUnCnM2KSX4XU5HmpodOA==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "es-shim-unscopables": "^1.0.0", + "get-intrinsic": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/array.prototype.flat": { "version": "1.3.2", "resolved": "https://registry.npmjs.org/array.prototype.flat/-/array.prototype.flat-1.3.2.tgz", @@ -3720,6 +3747,14 @@ "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", "dev": true }, + "node_modules/buffer-writer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/buffer-writer/-/buffer-writer-2.0.0.tgz", + "integrity": "sha512-a7ZpuTZU1TRtnwyCNW3I5dc0wWNC3VR9S++Ewyk2HHZdrO3CQJqSpd+95Us590V6AL7JqUAH2IwZ/398PmNFgw==", + "engines": { + "node": ">=4" + } + }, "node_modules/builtin-modules": { "version": "3.3.0", "resolved": "https://registry.npmjs.org/builtin-modules/-/builtin-modules-3.3.0.tgz", @@ -5037,6 +5072,52 @@ "eslint": ">=7.0.0" } }, + "node_modules/eslint-import-resolver-node": { + "version": "0.3.9", + "resolved": "https://registry.npmjs.org/eslint-import-resolver-node/-/eslint-import-resolver-node-0.3.9.tgz", + "integrity": "sha512-WFj2isz22JahUv+B788TlO3N6zL3nNJGU8CcZbPZvVEkBPaJdCV4vy5wyghty5ROFbCRnm132v8BScu5/1BQ8g==", + "dev": true, + "dependencies": { + "debug": "^3.2.7", + "is-core-module": "^2.13.0", + "resolve": "^1.22.4" + } + }, + "node_modules/eslint-import-resolver-node/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-module-utils": { + "version": "2.8.0", + "resolved": "https://registry.npmjs.org/eslint-module-utils/-/eslint-module-utils-2.8.0.tgz", + "integrity": "sha512-aWajIYfsqCKRDgUfjEXNN/JlrzauMuSEy5sbd7WXbtW3EH6A6MpwEh42c7qD+MqQo9QMJ6fWLAeIJynx0g6OAw==", + "dev": true, + "dependencies": { + "debug": "^3.2.7" + }, + "engines": { + "node": ">=4" + }, + "peerDependenciesMeta": { + "eslint": { + "optional": true + } + } + }, + "node_modules/eslint-module-utils/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, "node_modules/eslint-plugin-es-x": { "version": "7.5.0", "resolved": "https://registry.npmjs.org/eslint-plugin-es-x/-/eslint-plugin-es-x-7.5.0.tgz", @@ -5057,6 +5138,58 @@ "eslint": ">=8" } }, + "node_modules/eslint-plugin-import": { + "version": "2.29.1", + "resolved": "https://registry.npmjs.org/eslint-plugin-import/-/eslint-plugin-import-2.29.1.tgz", + "integrity": "sha512-BbPC0cuExzhiMo4Ff1BTVwHpjjv28C5R+btTOGaCRC7UEz801up0JadwkeSk5Ued6TG34uaczuVuH6qyy5YUxw==", + "dev": true, + "dependencies": { + "array-includes": "^3.1.7", + "array.prototype.findlastindex": "^1.2.3", + "array.prototype.flat": "^1.3.2", + "array.prototype.flatmap": "^1.3.2", + "debug": "^3.2.7", + "doctrine": "^2.1.0", + "eslint-import-resolver-node": "^0.3.9", + "eslint-module-utils": "^2.8.0", + "hasown": "^2.0.0", + "is-core-module": "^2.13.1", + "is-glob": "^4.0.3", + "minimatch": "^3.1.2", + "object.fromentries": "^2.0.7", + "object.groupby": "^1.0.1", + "object.values": "^1.1.7", + "semver": "^6.3.1", + "tsconfig-paths": "^3.15.0" + }, + "engines": { + "node": ">=4" + }, + "peerDependencies": { + "eslint": "^2 || ^3 || ^4 || ^5 || ^6 || ^7.2.0 || ^8" + } + }, + "node_modules/eslint-plugin-import/node_modules/debug": { + "version": "3.2.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-3.2.7.tgz", + "integrity": "sha512-CFjzYYAi4ThfiQvizrFQevTTXHtnCqWfe7x1AhgEscTz6ZbLbfoLRLPugTQyBth6f8ZERVUSyWHFD/7Wu4t1XQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.1" + } + }, + "node_modules/eslint-plugin-import/node_modules/doctrine": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/doctrine/-/doctrine-2.1.0.tgz", + "integrity": "sha512-35mSku4ZXK0vfCuHEDAwt55dg2jNajHZ1odvF+8SSr82EsZY4QmXfuWso8oEd8zRhVObSN18aM0CjSdoBX7zIw==", + "dev": true, + "dependencies": { + "esutils": "^2.0.2" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/eslint-plugin-jest": { "version": "27.6.3", "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-27.6.3.tgz", @@ -9073,6 +9206,15 @@ "node": "*" } }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, "node_modules/mlly": { "version": "1.5.0", "resolved": "https://registry.npmjs.org/mlly/-/mlly-1.5.0.tgz", @@ -9687,6 +9829,18 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/object.groupby": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/object.groupby/-/object.groupby-1.0.1.tgz", + "integrity": "sha512-HqaQtqLnp/8Bn4GL16cj+CUYbnpe1bh0TtEaWvybszDG4tgxCJuRpV8VGuvNaI1fAnI4lUJzDG55MXcOH4JZcQ==", + "dev": true, + "dependencies": { + "call-bind": "^1.0.2", + "define-properties": "^1.2.0", + "es-abstract": "^1.22.1", + "get-intrinsic": "^1.2.1" + } + }, "node_modules/object.hasown": { "version": "1.1.3", "resolved": "https://registry.npmjs.org/object.hasown/-/object.hasown-1.1.3.tgz", @@ -9944,6 +10098,11 @@ "node": ">=8" } }, + "node_modules/packet-reader": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/packet-reader/-/packet-reader-1.0.0.tgz", + "integrity": "sha512-HAKu/fG3HpHFO0AA8WE8q2g+gBJaZ9MG7fcKk+IJPLTGAD6Psw4443l+9DGRbOIh3/aXr7Phy0TjilYivJo5XQ==" + }, "node_modules/parent-module": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", @@ -10056,6 +10215,89 @@ "node": "*" } }, + "node_modules/pg": { + "version": "8.11.3", + "resolved": "https://registry.npmjs.org/pg/-/pg-8.11.3.tgz", + "integrity": "sha512-+9iuvG8QfaaUrrph+kpF24cXkH1YOOUeArRNYIxq1viYHZagBxrTno7cecY1Fa44tJeZvaoG+Djpkc3JwehN5g==", + "dependencies": { + "buffer-writer": "2.0.0", + "packet-reader": "1.0.0", + "pg-connection-string": "^2.6.2", + "pg-pool": "^3.6.1", + "pg-protocol": "^1.6.0", + "pg-types": "^2.1.0", + "pgpass": "1.x" + }, + "engines": { + "node": ">= 8.0.0" + }, + "optionalDependencies": { + "pg-cloudflare": "^1.1.1" + }, + "peerDependencies": { + "pg-native": ">=3.0.1" + }, + "peerDependenciesMeta": { + "pg-native": { + "optional": true + } + } + }, + "node_modules/pg-cloudflare": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/pg-cloudflare/-/pg-cloudflare-1.1.1.tgz", + "integrity": "sha512-xWPagP/4B6BgFO+EKz3JONXv3YDgvkbVrGw2mTo3D6tVDQRh1e7cqVGvyR3BE+eQgAvx1XhW/iEASj4/jCWl3Q==", + "optional": true + }, + "node_modules/pg-connection-string": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/pg-connection-string/-/pg-connection-string-2.6.2.tgz", + "integrity": "sha512-ch6OwaeaPYcova4kKZ15sbJ2hKb/VP48ZD2gE7i1J+L4MspCtBMAx8nMgz7bksc7IojCIIWuEhHibSMFH8m8oA==" + }, + "node_modules/pg-int8": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/pg-int8/-/pg-int8-1.0.1.tgz", + "integrity": "sha512-WCtabS6t3c8SkpDBUlb1kjOs7l66xsGdKpIPZsg4wR+B3+u9UAum2odSsF9tnvxg80h4ZxLWMy4pRjOsFIqQpw==", + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/pg-pool": { + "version": "3.6.1", + "resolved": "https://registry.npmjs.org/pg-pool/-/pg-pool-3.6.1.tgz", + "integrity": "sha512-jizsIzhkIitxCGfPRzJn1ZdcosIt3pz9Sh3V01fm1vZnbnCMgmGl5wvGGdNN2EL9Rmb0EcFoCkixH4Pu+sP9Og==", + "peerDependencies": { + "pg": ">=8.0" + } + }, + "node_modules/pg-protocol": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/pg-protocol/-/pg-protocol-1.6.0.tgz", + "integrity": "sha512-M+PDm637OY5WM307051+bsDia5Xej6d9IR4GwJse1qA1DIhiKlksvrneZOYQq42OM+spubpcNYEo2FcKQrDk+Q==" + }, + "node_modules/pg-types": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/pg-types/-/pg-types-2.2.0.tgz", + "integrity": "sha512-qTAAlrEsl8s4OiEQY69wDvcMIdQN6wdz5ojQiOy6YRMuynxenON0O5oCpJI6lshc6scgAY8qvJ2On/p+CXY0GA==", + "dependencies": { + "pg-int8": "1.0.1", + "postgres-array": "~2.0.0", + "postgres-bytea": "~1.0.0", + "postgres-date": "~1.0.4", + "postgres-interval": "^1.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/pgpass": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/pgpass/-/pgpass-1.0.5.tgz", + "integrity": "sha512-FdW9r/jQZhSeohs1Z3sI1yxFQNFvMcnmfuj4WBMUTxOrAyLMaTcE1aAMBiTlbMNaXvBCQuVi0R7hd8udDSP7ug==", + "dependencies": { + "split2": "^4.1.0" + } + }, "node_modules/picocolors": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.0.tgz", @@ -10242,6 +10484,41 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/postgres-array": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/postgres-array/-/postgres-array-2.0.0.tgz", + "integrity": "sha512-VpZrUqU5A69eQyW2c5CA1jtLecCsN2U/bD6VilrFDWq5+5UIEVO7nazS3TEcHf1zuPYO/sqGvUvW62g86RXZuA==", + "engines": { + "node": ">=4" + } + }, + "node_modules/postgres-bytea": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/postgres-bytea/-/postgres-bytea-1.0.0.tgz", + "integrity": "sha512-xy3pmLuQqRBZBXDULy7KbaitYqLcmxigw14Q5sj8QBVLqEwXfeybIKVWiqAXTlcvdvb0+xkOtDbfQMOf4lST1w==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-date": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/postgres-date/-/postgres-date-1.0.7.tgz", + "integrity": "sha512-suDmjLVQg78nMK2UZ454hAG+OAW+HQPZ6n++TNDUX+L0+uUlLywnoxJKDou51Zm+zTCjrCl0Nq6J9C5hP9vK/Q==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/postgres-interval": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/postgres-interval/-/postgres-interval-1.2.0.tgz", + "integrity": "sha512-9ZhXKM/rw350N1ovuWHbGxnGh/SNJ4cnxHiM0rxE4VN41wsg8P8zWn9hv/buK00RP4WvlOyr/RBDiptyxVbkZQ==", + "dependencies": { + "xtend": "^4.0.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/prelude-ls": { "version": "1.2.1", "resolved": "https://registry.npmjs.org/prelude-ls/-/prelude-ls-1.2.1.tgz", @@ -11258,6 +11535,14 @@ "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", "dev": true }, + "node_modules/split2": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/split2/-/split2-4.2.0.tgz", + "integrity": "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg==", + "engines": { + "node": ">= 10.x" + } + }, "node_modules/sprintf-js": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", @@ -11821,6 +12106,39 @@ "typescript": ">=4.2.0" } }, + "node_modules/tsconfig-paths": { + "version": "3.15.0", + "resolved": "https://registry.npmjs.org/tsconfig-paths/-/tsconfig-paths-3.15.0.tgz", + "integrity": "sha512-2Ac2RgzDe/cn48GvOe3M+o82pEFewD3UPbyoUHHdKasHwJKjds4fLXWf/Ux5kATBKN20oaFGu+jbElp1pos0mg==", + "dev": true, + "dependencies": { + "@types/json5": "^0.0.29", + "json5": "^1.0.2", + "minimist": "^1.2.6", + "strip-bom": "^3.0.0" + } + }, + "node_modules/tsconfig-paths/node_modules/json5": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/json5/-/json5-1.0.2.tgz", + "integrity": "sha512-g1MWMLBiz8FKi1e4w0UyVL3w+iJceWAFBAaBnnGKOpNa5f8TLktkbre1+s6oICydWAm+HRUGTmI+//xv2hvXYA==", + "dev": true, + "dependencies": { + "minimist": "^1.2.0" + }, + "bin": { + "json5": "lib/cli.js" + } + }, + "node_modules/tsconfig-paths/node_modules/strip-bom": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/strip-bom/-/strip-bom-3.0.0.tgz", + "integrity": "sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/tslib": { "version": "2.6.2", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", @@ -12644,6 +12962,14 @@ "integrity": "sha512-JZnDKK8B0RCDw84FNdDAIpZK+JuJw+s7Lz8nksI7SIuU3UXJJslUthsi+uWBUYOwPFwW7W7PRLRfUKpxjtjFCw==", "dev": true }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, "node_modules/y18n": { "version": "5.0.8", "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", diff --git a/package.json b/package.json index a871dbf7..fdeccd78 100644 --- a/package.json +++ b/package.json @@ -53,6 +53,7 @@ "cross-env": "^7.0.3", "eslint": "^8.56.0", "eslint-config-prettier": "^9.1.0", + "eslint-plugin-import": "^2.29.1", "husky": "^9.0.6", "lint-staged": "^15.2.0", "nyc": "^15.1.0", diff --git a/web/.eslintrc b/web/.eslintrc.json similarity index 100% rename from web/.eslintrc rename to web/.eslintrc.json diff --git a/web/vite.config.js b/web/vite.config.js index c84e6636..62916837 100644 --- a/web/vite.config.js +++ b/web/vite.config.js @@ -1,6 +1,6 @@ /* eslint-env node */ -import { defineConfig } from "vite"; import react from "@vitejs/plugin-react-swc"; +import { defineConfig } from "vite"; const apiPort = process.env.API_PORT ?? "3100"; From 5562d92ab7f2fbf1d32528c2cc9a2efb2b00b749 Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Tue, 30 Jan 2024 09:51:11 +0000 Subject: [PATCH 02/10] Add DB to Node.js E2E tests --- .github/workflows/push.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 672e4608..aafd8807 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -33,7 +33,11 @@ jobs: DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb FORCE_COLOR: true - run: npm run e2e + env: + DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb - run: npm run e2e:dev + env: + DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb docker: runs-on: ubuntu-latest steps: From bcd94af02c9e45f2b2e93c08dae890db5f5b6ced Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Tue, 30 Jan 2024 09:56:30 +0000 Subject: [PATCH 03/10] Add DB to Docker E2E tests --- .github/workflows/push.yml | 24 ++++++++++++++++++++++-- 1 file changed, 22 insertions(+), 2 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index aafd8807..74f7613e 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -40,6 +40,19 @@ jobs: DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb docker: runs-on: ubuntu-latest + services: + postgres: + image: postgres + env: + POSTGRES_PASSWORD: keepitsecret + POSTGRES_USER: testdb + options: >- + --health-cmd pg_isready + --health-interval 10s + --health-timeout 5s + --health-retries 5 + ports: + - 5432:5432 steps: - uses: textbook/take-action@nodejs with: @@ -51,12 +64,19 @@ jobs: load: true push: false tags: textbook/starter-kit:v2 + - id: env-file + run: | + echo "file=$ENV_FILE" >> $GITHUB_OUTPUTS + echo 'LOG_LEVEL=debug' >> $ENV_FILE + echo 'NODE_ENV=docker' >> $ENV_FILE + echo 'DATABASE_URL=postgres://testdb:keepitsecret@localhost:5432/testdb' >> $ENV_FILE + env: + ENV_FILE: docker.env - id: docker-run run: | echo "id=$(docker run \ --detach \ - --env LOG_LEVEL=debug \ - --env NODE_ENV=docker \ + --env-file ${{ steps.env-file.outputs.file }} \ --init \ --publish 4321:80 \ textbook/starter-kit:v2)" >> $GITHUB_OUTPUT From 390066972f849d4f1e02e361e2253c292e898031 Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Tue, 30 Jan 2024 09:59:25 +0000 Subject: [PATCH 04/10] Fix redirect --- .github/workflows/push.yml | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 74f7613e..6ec471bc 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -66,10 +66,10 @@ jobs: tags: textbook/starter-kit:v2 - id: env-file run: | - echo "file=$ENV_FILE" >> $GITHUB_OUTPUTS - echo 'LOG_LEVEL=debug' >> $ENV_FILE - echo 'NODE_ENV=docker' >> $ENV_FILE - echo 'DATABASE_URL=postgres://testdb:keepitsecret@localhost:5432/testdb' >> $ENV_FILE + echo "file=$ENV_FILE" >> "$GITHUB_OUTPUT" + echo 'DATABASE_URL=postgres://testdb:keepitsecret@localhost:5432/testdb' >> "$ENV_FILE" + echo 'LOG_LEVEL=debug' >> "$ENV_FILE" + echo 'NODE_ENV=docker' >> "$ENV_FILE" env: ENV_FILE: docker.env - id: docker-run From 502ae5ee0733dcdad6d6658aab05f8580826b1e4 Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Tue, 30 Jan 2024 10:13:15 +0000 Subject: [PATCH 05/10] Try using hostname --- .github/workflows/push.yml | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 6ec471bc..3f332a70 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -22,6 +22,8 @@ jobs: --health-retries 5 ports: - 5432:5432 + env: + DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb steps: - uses: textbook/take-action@nodejs with: @@ -30,14 +32,9 @@ jobs: - run: npm run lint - run: npm run test:cover env: - DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb FORCE_COLOR: true - run: npm run e2e - env: - DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb - run: npm run e2e:dev - env: - DATABASE_URL: postgres://testdb:keepitsecret@localhost:5432/testdb docker: runs-on: ubuntu-latest services: @@ -67,7 +64,7 @@ jobs: - id: env-file run: | echo "file=$ENV_FILE" >> "$GITHUB_OUTPUT" - echo 'DATABASE_URL=postgres://testdb:keepitsecret@localhost:5432/testdb' >> "$ENV_FILE" + echo 'DATABASE_URL=postgres://testdb:keepitsecret@postgres:5432/testdb' >> "$ENV_FILE" echo 'LOG_LEVEL=debug' >> "$ENV_FILE" echo 'NODE_ENV=docker' >> "$ENV_FILE" env: From 39df5247af89a734b822d27f3968c0c118e3f112 Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Wed, 31 Jan 2024 13:03:44 +0000 Subject: [PATCH 06/10] Try to run container on host network --- .github/workflows/push.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 3f332a70..8e4408fc 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -64,7 +64,7 @@ jobs: - id: env-file run: | echo "file=$ENV_FILE" >> "$GITHUB_OUTPUT" - echo 'DATABASE_URL=postgres://testdb:keepitsecret@postgres:5432/testdb' >> "$ENV_FILE" + echo 'DATABASE_URL=postgres://testdb:keepitsecret@localhost:5432/testdb' >> "$ENV_FILE" echo 'LOG_LEVEL=debug' >> "$ENV_FILE" echo 'NODE_ENV=docker' >> "$ENV_FILE" env: @@ -75,6 +75,7 @@ jobs: --detach \ --env-file ${{ steps.env-file.outputs.file }} \ --init \ + --network 'host' \ --publish 4321:80 \ textbook/starter-kit:v2)" >> $GITHUB_OUTPUT - run: npm run e2e From d70050d2b542f5b33dc13108f1bc25cad92db79c Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Wed, 31 Jan 2024 15:36:24 +0000 Subject: [PATCH 07/10] Don't try to use port 80 on the runner --- .github/workflows/push.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 8e4408fc..4f47caf1 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -66,6 +66,7 @@ jobs: echo "file=$ENV_FILE" >> "$GITHUB_OUTPUT" echo 'DATABASE_URL=postgres://testdb:keepitsecret@localhost:5432/testdb' >> "$ENV_FILE" echo 'LOG_LEVEL=debug' >> "$ENV_FILE" + echo 'PORT=4321' >> "$ENV_FILE" echo 'NODE_ENV=docker' >> "$ENV_FILE" env: ENV_FILE: docker.env @@ -76,7 +77,7 @@ jobs: --env-file ${{ steps.env-file.outputs.file }} \ --init \ --network 'host' \ - --publish 4321:80 \ + --publish 4321:4321 \ textbook/starter-kit:v2)" >> $GITHUB_OUTPUT - run: npm run e2e env: From 12bc96a235ea5f3c85fa9def31e7d7f9a9acde5d Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Wed, 31 Jan 2024 19:57:19 +0000 Subject: [PATCH 08/10] Manage SSL locally/in Heroku --- api/db.js | 12 +++++++++++- api/utils/config.js | 2 +- 2 files changed, 12 insertions(+), 2 deletions(-) diff --git a/api/db.js b/api/db.js index e4e95385..a38b597a 100644 --- a/api/db.js +++ b/api/db.js @@ -3,9 +3,19 @@ import { default as pg } from "pg"; import config from "./utils/config.js"; import logger from "./utils/logger.js"; +const databaseUrl = new URL(config.databaseUrl); + +const localDb = ["0.0.0.0", "127.0.0.1", "localhost"].includes( + databaseUrl.hostname, +); +const sslMode = ["prefer", "require", "verify-ca", "verify-full"].includes( + databaseUrl.searchParams.get("sslmode") ?? "none", +); + const pool = new pg.Pool({ - connectionString: config.databaseUrl, + connectionString: databaseUrl.toString(), connectionTimeoutMillis: 5_000, + ssl: localDb ? false : { rejectUnauthorized: sslMode }, }); pool.on("error", (err) => logger.error("%O", err)); diff --git a/api/utils/config.js b/api/utils/config.js index 31227426..127c74f9 100644 --- a/api/utils/config.js +++ b/api/utils/config.js @@ -14,7 +14,7 @@ configDotenv({ path: dotenvPath }); requireArgs(["DATABASE_URL"]); /** - * @property {string} databaseUrl + * @property {URL} databaseUrl * @property {string} dotenvPath * @property {string} logLevel * @property {number} port From fce5fdb02cb4cc9532d3abcec1c9912c687c55a5 Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Thu, 1 Feb 2024 10:45:44 +0000 Subject: [PATCH 09/10] Minor readability tweaks --- .github/workflows/push.yml | 2 +- api/db.js | 4 ++-- api/server.js | 8 +++----- api/setupTests.js | 6 +++--- 4 files changed, 9 insertions(+), 11 deletions(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 4f47caf1..789dc5e7 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -63,11 +63,11 @@ jobs: tags: textbook/starter-kit:v2 - id: env-file run: | - echo "file=$ENV_FILE" >> "$GITHUB_OUTPUT" echo 'DATABASE_URL=postgres://testdb:keepitsecret@localhost:5432/testdb' >> "$ENV_FILE" echo 'LOG_LEVEL=debug' >> "$ENV_FILE" echo 'PORT=4321' >> "$ENV_FILE" echo 'NODE_ENV=docker' >> "$ENV_FILE" + echo "file=$ENV_FILE" >> "$GITHUB_OUTPUT" env: ENV_FILE: docker.env - id: docker-run diff --git a/api/db.js b/api/db.js index a38b597a..623bb4b8 100644 --- a/api/db.js +++ b/api/db.js @@ -20,13 +20,13 @@ const pool = new pg.Pool({ pool.on("error", (err) => logger.error("%O", err)); -export const connect = async () => { +export const connectDb = async () => { const client = await pool.connect(); logger.info("connected to %s", client.database); client.release(); }; -export const disconnect = async () => await pool.end(); +export const disconnectDb = async () => await pool.end(); export default { query(...args) { diff --git a/api/server.js b/api/server.js index 77d5362e..83f5ccb7 100644 --- a/api/server.js +++ b/api/server.js @@ -1,12 +1,10 @@ import app from "./app.js"; -import { connect } from "./db.js"; +import { connectDb } from "./db.js"; import config from "./utils/config.js"; import logger from "./utils/logger.js"; const { port } = config; -await connect(); +await connectDb(); -const server = app.listen(port); - -server.on("listening", () => logger.info(`listening on ${port}`)); +app.listen(port, () => logger.info(`listening on ${port}`)); diff --git a/api/setupTests.js b/api/setupTests.js index 70a7274e..f0de6d42 100644 --- a/api/setupTests.js +++ b/api/setupTests.js @@ -1,5 +1,5 @@ -import { connect, disconnect } from "./db.js"; +import { connectDb, disconnectDb } from "./db.js"; -beforeAll(() => connect()); +beforeAll(() => connectDb()); -afterAll(() => disconnect()); +afterAll(() => disconnectDb()); From e497df3c1a98bd8187dc620e39f167e51b694929 Mon Sep 17 00:00:00 2001 From: Jonathan Sharpe Date: Thu, 1 Feb 2024 10:59:07 +0000 Subject: [PATCH 10/10] Try to make Docker build more stable --- .github/workflows/push.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/push.yml b/.github/workflows/push.yml index 789dc5e7..f6dd64f4 100644 --- a/.github/workflows/push.yml +++ b/.github/workflows/push.yml @@ -77,8 +77,8 @@ jobs: --env-file ${{ steps.env-file.outputs.file }} \ --init \ --network 'host' \ - --publish 4321:4321 \ textbook/starter-kit:v2)" >> $GITHUB_OUTPUT + - run: npx --yes wait-on --log --timeout 30000 http-get://localhost:4321 - run: npm run e2e env: PLAYWRIGHT_BASE_URL: http://localhost:4321