From f280c165ea54efe82238dc802077f1dcae51d466 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 13:38:08 +0200 Subject: [PATCH 001/100] chore: create basic architecture --- .github/dependabot.yml | 13 ++++++++++++ .github/stale.yml | 21 ++++++++++++++++++ .github/workflows/ci.yml | 31 +++++++++++++++++++++++++++ .gitignore | 6 ++++++ README.md | 41 +++++++++++++++++++++++++++++++++++- app.js | 26 +++++++++++++++++++++++ package.json | 31 +++++++++++++++++++++++++++ plugins/README.md | 13 ++++++++++++ plugins/sensible.js | 13 ++++++++++++ plugins/support.js | 10 +++++++++ routes/README.md | 27 ++++++++++++++++++++++++ routes/example/index.js | 6 ++++++ routes/root.js | 5 +++++ test/helper.js | 29 +++++++++++++++++++++++++ test/plugins/support.test.js | 26 +++++++++++++++++++++++ test/routes/example.test.js | 27 ++++++++++++++++++++++++ test/routes/root.test.js | 26 +++++++++++++++++++++++ 17 files changed, 350 insertions(+), 1 deletion(-) create mode 100644 .github/dependabot.yml create mode 100644 .github/stale.yml create mode 100644 .github/workflows/ci.yml create mode 100644 app.js create mode 100644 package.json create mode 100644 plugins/README.md create mode 100644 plugins/sensible.js create mode 100644 plugins/support.js create mode 100644 routes/README.md create mode 100644 routes/example/index.js create mode 100644 routes/root.js create mode 100644 test/helper.js create mode 100644 test/plugins/support.test.js create mode 100644 test/routes/example.test.js create mode 100644 test/routes/root.test.js diff --git a/.github/dependabot.yml b/.github/dependabot.yml new file mode 100644 index 00000000..dfa7fa6c --- /dev/null +++ b/.github/dependabot.yml @@ -0,0 +1,13 @@ +version: 2 +updates: + - package-ecosystem: "github-actions" + directory: "/" + schedule: + interval: "monthly" + open-pull-requests-limit: 10 + + - package-ecosystem: "npm" + directory: "/" + schedule: + interval: "weekly" + open-pull-requests-limit: 10 diff --git a/.github/stale.yml b/.github/stale.yml new file mode 100644 index 00000000..d51ce639 --- /dev/null +++ b/.github/stale.yml @@ -0,0 +1,21 @@ +# Number of days of inactivity before an issue becomes stale +daysUntilStale: 15 +# Number of days of inactivity before a stale issue is closed +daysUntilClose: 7 +# Issues with these labels will never be considered stale +exemptLabels: + - "discussion" + - "feature request" + - "bug" + - "help wanted" + - "plugin suggestion" + - "good first issue" +# Label to use when marking an issue as stale +staleLabel: stale +# Comment to post when marking an issue as stale. Set to `false` to disable +markComment: > + This issue has been automatically marked as stale because it has not had + recent activity. It will be closed if no further activity occurs. Thank you + for your contributions. +# Comment to post when closing a stale issue. Set to `false` to disable +closeComment: false diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml new file mode 100644 index 00000000..7a1316ff --- /dev/null +++ b/.github/workflows/ci.yml @@ -0,0 +1,31 @@ +name: Fastify Official Demo CI + +on: + push: + branches: [ "main" ] + pull_request: + branches: [ "main" ] + +jobs: + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [22] + + steps: + - uses: actions/checkout@v3 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v3 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm i --ignore-scripts + + - name: Lint Code + run: npm run lint + + - name: Test + run: npm test diff --git a/.gitignore b/.gitignore index c6bba591..48ea4aeb 100644 --- a/.gitignore +++ b/.gitignore @@ -128,3 +128,9 @@ dist .yarn/build-state.yml .yarn/install-state.gz .pnp.* + +# lock files +bun.lockb +package-lock.json +pnpm-lock.yaml +yarn.lock diff --git a/README.md b/README.md index 97be1367..ec56dea4 100644 --- a/README.md +++ b/README.md @@ -1 +1,40 @@ -# demo \ No newline at end of file +# Fastify Official Demo + +![CI](https://github.com/fastify/demo/workflows/CI/badge.svg) +[![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) + +> :warning: **Please note:** This repository is still under active development. + +The aim of this repository is to provide a concrete example of a Fastify application using what are considered best practices by the Fastify community. + +**Prerequisites:** You need to have Node.js version 22 or higher installed. + +## Getting started +Install [`fastify-cli`](https://github.com/fastify/fastify-cli): +``` +npm install fastify-cli --global +``` + +## Available Scripts +In the project directory, you can run: + +### `npm run dev` + +To start the app in dev mode.\ +Open [http://localhost:3000](http://localhost:3000) to view it in the browser. + +### `npm start` + +For production mode + +### `npm run test` + +Run the test cases. + +## Learn More + +To learn Fastify, check out the [Fastify documentation](https://fastify.dev/docs/latest/). + + + + diff --git a/app.js b/app.js new file mode 100644 index 00000000..68146df5 --- /dev/null +++ b/app.js @@ -0,0 +1,26 @@ +import path from 'node:path' +import AutoLoad from '@fastify/autoload' + +// Pass --options via CLI arguments in command to enable these options. +export const options = {} + +export default async function app (fastify, opts) { + // Place here your custom code! + + // Do not touch the following lines + + // This loads all plugins defined in plugins + // those should be support plugins that are reused + // through your application + fastify.register(AutoLoad, { + dir: path.join(import.meta.dirname, 'plugins'), + options: { ...opts } + }) + + // This loads all plugins defined in routes + // define your routes in one of these + fastify.register(AutoLoad, { + dir: path.join(import.meta.dirname, 'routes'), + options: { ...opts } + }) +} diff --git a/package.json b/package.json new file mode 100644 index 00000000..f39c3da1 --- /dev/null +++ b/package.json @@ -0,0 +1,31 @@ +{ + "name": "fastify-demo", + "version": "0.0.0", + "description": "The official Fastify demo!", + "main": "app.js", + "type": "module", + "directories": { + "test": "test" + }, + "scripts": { + "test": "cross-env node --test test/**/*.test.js", + "start": "fastify start -l info app.js", + "dev": "fastify start -w -l info -P app.js", + "lint": "standard", + "lint:fix": "npm run lint -- --fix" + }, + "keywords": [], + "author": "Michelet Jean ", + "license": "MIT", + "dependencies": { + "@fastify/autoload": "^5.0.0", + "@fastify/sensible": "^5.0.0", + "fastify": "^4.26.1", + "fastify-cli": "^6.1.1", + "fastify-plugin": "^4.0.0" + }, + "devDependencies": { + "cross-env": "^7.0.3", + "standard": "^17.1.0" + } +} diff --git a/plugins/README.md b/plugins/README.md new file mode 100644 index 00000000..76b414d6 --- /dev/null +++ b/plugins/README.md @@ -0,0 +1,13 @@ +# Plugins Folder + +Plugins define behavior that is common to all the routes in your +application. Authentication, caching, templates, and all the other cross cutting concerns should be handled by plugins placed in this folder. + +Files in this folder are typically defined through the +[`fastify-plugin`](https://github.com/fastify/fastify-plugin module, making them non-encapsulated. They can define decorators and set hooks that will then be used in the rest of your application. + +Check out: + +* [The hitchhiker's guide to plugins](https://fastify.dev/docs/latest/Guides/Plugins-Guide/) +* [Fastify decorators](https://fastify.dev/docs/latest/Reference/Decorators/). +* [Fastify lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/). diff --git a/plugins/sensible.js b/plugins/sensible.js new file mode 100644 index 00000000..3ae2a632 --- /dev/null +++ b/plugins/sensible.js @@ -0,0 +1,13 @@ +import fp from 'fastify-plugin' +import sensible from '@fastify/sensible' + +/** + * This plugins adds some utilities to handle http errors + * + * @see https://github.com/fastify/fastify-sensible + */ +export default fp(async function (fastify, opts) { + fastify.register(sensible, { + errorHandler: false + }) +}) diff --git a/plugins/support.js b/plugins/support.js new file mode 100644 index 00000000..a17d8b87 --- /dev/null +++ b/plugins/support.js @@ -0,0 +1,10 @@ +import fp from 'fastify-plugin' + +// the use of fastify-plugin is required to be able +// to export the decorators to the outer scope + +export default fp(async function (fastify, opts) { + fastify.decorate('someSupport', function () { + return 'hugs' + }) +}) diff --git a/routes/README.md b/routes/README.md new file mode 100644 index 00000000..fd8d0b2a --- /dev/null +++ b/routes/README.md @@ -0,0 +1,27 @@ +# Routes Folder + +Routes define routes within your application. Fastify provides an +easy path to a microservice architecture, in the future you might want +to independently deploy some of those. + +In this folder you should define all the routes that define the endpoints +of your web application. +Each service is a [Fastify +plugin](https://fastify.dev/docs/latest/Reference/Plugins/), it is +encapsulated (it can have its own independent plugins) and it is +typically stored in a file; be careful to group your routes logically, +e.g. all `/users` routes in a `users.js` file. We have added +a `root.js` file for you with a '/' root added. + +If a single file become too large, create a folder and add a `index.js` file there: +this file must be a Fastify plugin, and it will be loaded automatically +by the application. You can now add as many files as you want inside that folder. +In this way you can create complex routes within a single monolith, +and eventually extract them. + +If you need to share functionality between routes, place that +functionality into the `plugins` folder, and share it via +[decorators](https://fastify.dev/docs/latest/Reference/Decorators/). + +If you're a bit confused about using `async/await` to write routes, you would +better take a look at [Promise resolution](https://fastify.dev/docs/latest/Reference/Routes/#promise-resolution) for more details. diff --git a/routes/example/index.js b/routes/example/index.js new file mode 100644 index 00000000..f28d136f --- /dev/null +++ b/routes/example/index.js @@ -0,0 +1,6 @@ +export default async function (fastify, opts) { + // is prefixed with /example by the autoloader + fastify.get('/', async function (request, reply) { + return 'this is an example' + }) +} diff --git a/routes/root.js b/routes/root.js new file mode 100644 index 00000000..b8e4cbce --- /dev/null +++ b/routes/root.js @@ -0,0 +1,5 @@ +export default async function (fastify, opts) { + fastify.get('/', async function (request, reply) { + return { root: true } + }) +} diff --git a/test/helper.js b/test/helper.js new file mode 100644 index 00000000..ee5aff55 --- /dev/null +++ b/test/helper.js @@ -0,0 +1,29 @@ +// This file contains code that we reuse +// between our tests. + +import { build as buildApplication } from 'fastify-cli/helper.js' +import path from 'node:path' + +const AppPath = path.join(import.meta.dirname, '../app.js') + +// Fill in this config with all the configurations +// needed for testing the application +export function config () { + return {} +} + +// automatically build and tear down our instance +export async function build (t) { + // you can set all the options supported by the fastify CLI command + const argv = [AppPath] + + // fastify-plugin ensures that all decorators + // are exposed for testing purposes, this is + // different from the production setup + const app = await buildApplication(argv, config()) + + // close the app after we are done + t.after(() => app.close()) + + return app +} diff --git a/test/plugins/support.test.js b/test/plugins/support.test.js new file mode 100644 index 00000000..1de4e2ed --- /dev/null +++ b/test/plugins/support.test.js @@ -0,0 +1,26 @@ +import { test } from 'node:test' +import assert from 'node:assert' + +import Fastify from 'fastify' +import Support from '../../plugins/support.js' + +test('support works standalone', async (t) => { + const fastify = Fastify() + fastify.register(Support) + + await fastify.ready() + assert.equal(fastify.someSupport(), 'hugs') +}) + +// You can also use plugin with opts in fastify v2 +// +// test('support works standalone', (t) => { +// t.plan(2) +// const fastify = Fastify() +// fastify.register(Support) +// +// fastify.ready((err) => { +// t.error(err) +// assert.equal(fastify.someSupport(), 'hugs') +// }) +// }) diff --git a/test/routes/example.test.js b/test/routes/example.test.js new file mode 100644 index 00000000..7beed53f --- /dev/null +++ b/test/routes/example.test.js @@ -0,0 +1,27 @@ +import { test } from 'node:test' +import assert from 'node:assert' +import { build } from '../helper.js' + +test('example is loaded', async (t) => { + const app = await build(t) + + const res = await app.inject({ + url: '/example' + }) + + assert.equal(res.payload, 'this is an example') +}) + +// inject callback style: +// +// test('example is loaded', (t) => { +// t.plan(2) +// const app = await build(t) +// +// app.inject({ +// url: '/example' +// }, (err, res) => { +// t.error(err) +// assert.equal(res.payload, 'this is an example') +// }) +// }) diff --git a/test/routes/root.test.js b/test/routes/root.test.js new file mode 100644 index 00000000..5bf5ba2a --- /dev/null +++ b/test/routes/root.test.js @@ -0,0 +1,26 @@ +import { test } from 'node:test' +import assert from 'node:assert' +import { build } from '../helper.js' + +test('default root route', async (t) => { + const app = await build(t) + + const res = await app.inject({ + url: '/' + }) + assert.deepStrictEqual(JSON.parse(res.payload), { root: true }) +}) + +// inject callback style: +// +// test('default root route', (t) => { +// t.plan(2) +// const app = await build(t) +// +// app.inject({ +// url: '/' +// }, (err, res) => { +// t.error(err) +// assert.deepStrictEqual(JSON.parse(res.payload), { root: true }) +// }) +// }) From 215955f7d83c3552f16385867cc0971867a9f428 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 13:53:35 +0200 Subject: [PATCH 002/100] fix: typo in comment --- routes/example/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/example/index.js b/routes/example/index.js index f28d136f..e0e17343 100644 --- a/routes/example/index.js +++ b/routes/example/index.js @@ -1,5 +1,5 @@ export default async function (fastify, opts) { - // is prefixed with /example by the autoloader + // Prefixed with /example by the autoloader fastify.get('/', async function (request, reply) { return 'this is an example' }) From 7f7392c01b81f48c8976986872495e3354b04490 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 14:20:35 +0200 Subject: [PATCH 003/100] chore: add server.js to show how to not depend on cli --- app.js | 12 +++++------ routes/{root.js => home.js} | 2 +- server.js | 42 +++++++++++++++++++++++++++++++++++++ test/routes/example.test.js | 14 ------------- test/routes/home.test.js | 12 +++++++++++ test/routes/root.test.js | 26 ----------------------- 6 files changed, 60 insertions(+), 48 deletions(-) rename routes/{root.js => home.js} (67%) create mode 100644 server.js create mode 100644 test/routes/home.test.js delete mode 100644 test/routes/root.test.js diff --git a/app.js b/app.js index 68146df5..0cd5e8eb 100644 --- a/app.js +++ b/app.js @@ -1,14 +1,12 @@ +/** + * If you would like to turn your application into a standalone executable, lookt at server.js file + * + */ + import path from 'node:path' import AutoLoad from '@fastify/autoload' -// Pass --options via CLI arguments in command to enable these options. -export const options = {} - export default async function app (fastify, opts) { - // Place here your custom code! - - // Do not touch the following lines - // This loads all plugins defined in plugins // those should be support plugins that are reused // through your application diff --git a/routes/root.js b/routes/home.js similarity index 67% rename from routes/root.js rename to routes/home.js index b8e4cbce..4c9ce5af 100644 --- a/routes/root.js +++ b/routes/home.js @@ -1,5 +1,5 @@ export default async function (fastify, opts) { fastify.get('/', async function (request, reply) { - return { root: true } + return 'Welcome to the official fastify demo!' }) } diff --git a/server.js b/server.js new file mode 100644 index 00000000..b68ed315 --- /dev/null +++ b/server.js @@ -0,0 +1,42 @@ +/** + * This file is here only to show you what to proceed if + * you would like to turn your application into a standalone executable. + */ + +import Fastify from 'fastify' + +// Import library to exit fastify process, gracefully (if possible) +import closeWithGrace from 'close-with-grace' + +// Import your application as a normal plugin. +import appService from './app.js' + + +// Instantiate Fastify with some config +const app = Fastify({ + logger: true +}) + +// Register your application as a normal plugin. +app.register(appService) + +// delay is the number of milliseconds for the graceful close to finish +const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY || 500 }, async function ({ signal, err, manual }) { + if (err) { + app.log.error(err) + } + await app.close() +}) + +app.addHook('onClose', (instance, done) => { + closeListeners.uninstall() + done() +}) + +// Start listening. +app.listen({ port: process.env.PORT || 3000 }, (err) => { + if (err) { + app.log.error(err) + process.exit(1) + } +}) diff --git a/test/routes/example.test.js b/test/routes/example.test.js index 7beed53f..bd4b7232 100644 --- a/test/routes/example.test.js +++ b/test/routes/example.test.js @@ -11,17 +11,3 @@ test('example is loaded', async (t) => { assert.equal(res.payload, 'this is an example') }) - -// inject callback style: -// -// test('example is loaded', (t) => { -// t.plan(2) -// const app = await build(t) -// -// app.inject({ -// url: '/example' -// }, (err, res) => { -// t.error(err) -// assert.equal(res.payload, 'this is an example') -// }) -// }) diff --git a/test/routes/home.test.js b/test/routes/home.test.js new file mode 100644 index 00000000..a6a82999 --- /dev/null +++ b/test/routes/home.test.js @@ -0,0 +1,12 @@ +import { test } from 'node:test' +import assert from 'node:assert' +import { build } from '../helper.js' + +test('default root route', async (t) => { + const app = await build(t) + + const res = await app.inject({ + url: '/' + }) + assert.deepStrictEqual(JSON.parse(res.payload), 'Welcome to the official fastify demo!') +}) diff --git a/test/routes/root.test.js b/test/routes/root.test.js deleted file mode 100644 index 5bf5ba2a..00000000 --- a/test/routes/root.test.js +++ /dev/null @@ -1,26 +0,0 @@ -import { test } from 'node:test' -import assert from 'node:assert' -import { build } from '../helper.js' - -test('default root route', async (t) => { - const app = await build(t) - - const res = await app.inject({ - url: '/' - }) - assert.deepStrictEqual(JSON.parse(res.payload), { root: true }) -}) - -// inject callback style: -// -// test('default root route', (t) => { -// t.plan(2) -// const app = await build(t) -// -// app.inject({ -// url: '/' -// }, (err, res) => { -// t.error(err) -// assert.deepStrictEqual(JSON.parse(res.payload), { root: true }) -// }) -// }) From 39c46b714e25f070973dc3e142e6306786064532 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 14:23:22 +0200 Subject: [PATCH 004/100] fix: should not try to parser string as json --- app.js | 2 +- server.js | 3 +-- test/routes/home.test.js | 2 +- 3 files changed, 3 insertions(+), 4 deletions(-) diff --git a/app.js b/app.js index 0cd5e8eb..e03573da 100644 --- a/app.js +++ b/app.js @@ -1,6 +1,6 @@ /** * If you would like to turn your application into a standalone executable, lookt at server.js file - * + * */ import path from 'node:path' diff --git a/server.js b/server.js index b68ed315..d0307cab 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,5 @@ /** - * This file is here only to show you what to proceed if + * This file is here only to show you what to proceed if * you would like to turn your application into a standalone executable. */ @@ -11,7 +11,6 @@ import closeWithGrace from 'close-with-grace' // Import your application as a normal plugin. import appService from './app.js' - // Instantiate Fastify with some config const app = Fastify({ logger: true diff --git a/test/routes/home.test.js b/test/routes/home.test.js index a6a82999..47d46c6b 100644 --- a/test/routes/home.test.js +++ b/test/routes/home.test.js @@ -8,5 +8,5 @@ test('default root route', async (t) => { const res = await app.inject({ url: '/' }) - assert.deepStrictEqual(JSON.parse(res.payload), 'Welcome to the official fastify demo!') + assert.deepStrictEqual(res.payload, 'Welcome to the official fastify demo!') }) From 1eca5f7b5887f69ee2ec7cbf267d2816b3f309e0 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 14:25:07 +0200 Subject: [PATCH 005/100] fix: typo comment --- server.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/server.js b/server.js index d0307cab..81c15544 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,6 @@ /** - * This file is here only to show you what to proceed if - * you would like to turn your application into a standalone executable. + * This file is here only to show you how to proceed if you would + * like to turn your application into a standalone executable. */ import Fastify from 'fastify' From a707ce96c793c5e07609f79f3254a35db63e086f Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 14:54:38 +0200 Subject: [PATCH 006/100] chore: add helmet plugin --- plugins/helmet.js | 13 +++++++++++++ server.js | 2 +- 2 files changed, 14 insertions(+), 1 deletion(-) create mode 100644 plugins/helmet.js diff --git a/plugins/helmet.js b/plugins/helmet.js new file mode 100644 index 00000000..a0b6a86c --- /dev/null +++ b/plugins/helmet.js @@ -0,0 +1,13 @@ +import fp from 'fastify-plugin' +import helmet from '@fastify/helmet' + +/** + * This plugins sets the basic security headers. + * + * @see https://github.com/fastify/fastify-helmet + */ +export default fp(async function (fastify, opts) { + fastify.register(helmet, { + // Set plugin options here + }) +}) diff --git a/server.js b/server.js index 81c15544..f58e21ee 100644 --- a/server.js +++ b/server.js @@ -1,5 +1,5 @@ /** - * This file is here only to show you how to proceed if you would + * This file is here only to show you how to proceed if you would * like to turn your application into a standalone executable. */ From 02895536353cc9fe9cbb28926becf32ac92cd23b Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 14:57:04 +0200 Subject: [PATCH 007/100] chore: add cors plugin --- package.json | 2 ++ plugins/core.js | 13 +++++++++++++ 2 files changed, 15 insertions(+) create mode 100644 plugins/core.js diff --git a/package.json b/package.json index f39c3da1..b78ae17d 100644 --- a/package.json +++ b/package.json @@ -19,6 +19,8 @@ "license": "MIT", "dependencies": { "@fastify/autoload": "^5.0.0", + "@fastify/cors": "^9.0.1", + "@fastify/helmet": "^11.1.1", "@fastify/sensible": "^5.0.0", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", diff --git a/plugins/core.js b/plugins/core.js new file mode 100644 index 00000000..c91dafda --- /dev/null +++ b/plugins/core.js @@ -0,0 +1,13 @@ +import fp from 'fastify-plugin' +import helmet from '@fastify/cors' + +/** + * This plugins enables the use of CORS. + * + * @see https://github.com/fastify/fastify-cors + */ +export default fp(async function (fastify, opts) { + fastify.register(helmet, { + // Set plugin options here + }) +}) From a3822576588eaf83c0cc4f5e7264cc90086bdfc3 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 15:32:54 +0200 Subject: [PATCH 008/100] chore: add env plugin --- package.json | 1 + plugins/core.js | 4 ++-- plugins/env.js | 44 +++++++++++++++++++++++++++++++++++++++++ server.js | 52 +++++++++++++++++++++++++++---------------------- 4 files changed, 76 insertions(+), 25 deletions(-) create mode 100644 plugins/env.js diff --git a/package.json b/package.json index b78ae17d..1306244e 100644 --- a/package.json +++ b/package.json @@ -20,6 +20,7 @@ "dependencies": { "@fastify/autoload": "^5.0.0", "@fastify/cors": "^9.0.1", + "@fastify/env": "^4.3.0", "@fastify/helmet": "^11.1.1", "@fastify/sensible": "^5.0.0", "fastify": "^4.26.1", diff --git a/plugins/core.js b/plugins/core.js index c91dafda..f1b32334 100644 --- a/plugins/core.js +++ b/plugins/core.js @@ -1,5 +1,5 @@ import fp from 'fastify-plugin' -import helmet from '@fastify/cors' +import cors from '@fastify/cors' /** * This plugins enables the use of CORS. @@ -7,7 +7,7 @@ import helmet from '@fastify/cors' * @see https://github.com/fastify/fastify-cors */ export default fp(async function (fastify, opts) { - fastify.register(helmet, { + fastify.register(cors, { // Set plugin options here }) }) diff --git a/plugins/env.js b/plugins/env.js new file mode 100644 index 00000000..4cbc1cc2 --- /dev/null +++ b/plugins/env.js @@ -0,0 +1,44 @@ +import fp from 'fastify-plugin' +import env from '@fastify/env' + +const schema = { + type: 'object', + required: ['PORT'], + // will read .env in root folder + properties: { + PORT: { + type: 'string', + default: 3000 + } + } +} + +const options = { + // Decorate Fastify instance with `config` key + // Optional, default: 'config' + confKey: 'config', + + // Schema to validate + schema, + + // Needed to read .env in root folder + dotenv: true, + // or, pass config options available on dotenv module + // dotenv: { + // path: `${__dirname}/.env`, + // debug: true + // } + + // Source for the configuration data + // Optional, default: process.env + data: process.env +} + +/** + * This plugins helps to check environment variables. + * + * @see https://github.com/fastify/fastify-env + */ +export default fp(async function (fastify, opts) { + fastify.register(env, options) +}) diff --git a/server.js b/server.js index f58e21ee..82b5e86e 100644 --- a/server.js +++ b/server.js @@ -16,26 +16,32 @@ const app = Fastify({ logger: true }) -// Register your application as a normal plugin. -app.register(appService) - -// delay is the number of milliseconds for the graceful close to finish -const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY || 500 }, async function ({ signal, err, manual }) { - if (err) { - app.log.error(err) - } - await app.close() -}) - -app.addHook('onClose', (instance, done) => { - closeListeners.uninstall() - done() -}) - -// Start listening. -app.listen({ port: process.env.PORT || 3000 }, (err) => { - if (err) { - app.log.error(err) - process.exit(1) - } -}) +async function init () { + // Register your application as a normal plugin. + app.register(appService) + + // delay is the number of milliseconds for the graceful close to finish + const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY || 500 }, async function ({ signal, err, manual }) { + if (err) { + app.log.error(err) + } + await app.close() + }) + + app.addHook('onClose', (instance, done) => { + closeListeners.uninstall() + done() + }) + + await app.ready() + + // Start listening. + app.listen({ port: process.env.PORT || 3000 }, (err) => { + if (err) { + app.log.error(err) + process.exit(1) + } + }) +} + +init() From 36156f45b7ba4d37af27a4fdb0dcf384aafa2bd0 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 15:39:49 +0200 Subject: [PATCH 009/100] refactor: reorganize plugin structure --- plugins/external/community/README.md | 1 + plugins/external/core/README.md | 1 + plugins/{core.js => external/core/cors.js} | 0 plugins/{ => external/core}/env.js | 0 plugins/{ => external/core}/helmet.js | 0 plugins/{ => external/core}/sensible.js | 2 +- plugins/support.js | 9 ++++++--- 7 files changed, 9 insertions(+), 4 deletions(-) create mode 100644 plugins/external/community/README.md create mode 100644 plugins/external/core/README.md rename plugins/{core.js => external/core/cors.js} (100%) rename plugins/{ => external/core}/env.js (100%) rename plugins/{ => external/core}/helmet.js (100%) rename plugins/{ => external/core}/sensible.js (81%) diff --git a/plugins/external/community/README.md b/plugins/external/community/README.md new file mode 100644 index 00000000..3936b30a --- /dev/null +++ b/plugins/external/community/README.md @@ -0,0 +1 @@ +Fastify community plugins configuration. \ No newline at end of file diff --git a/plugins/external/core/README.md b/plugins/external/core/README.md new file mode 100644 index 00000000..d5551c76 --- /dev/null +++ b/plugins/external/core/README.md @@ -0,0 +1 @@ +Fastify core plugins configuration. \ No newline at end of file diff --git a/plugins/core.js b/plugins/external/core/cors.js similarity index 100% rename from plugins/core.js rename to plugins/external/core/cors.js diff --git a/plugins/env.js b/plugins/external/core/env.js similarity index 100% rename from plugins/env.js rename to plugins/external/core/env.js diff --git a/plugins/helmet.js b/plugins/external/core/helmet.js similarity index 100% rename from plugins/helmet.js rename to plugins/external/core/helmet.js diff --git a/plugins/sensible.js b/plugins/external/core/sensible.js similarity index 81% rename from plugins/sensible.js rename to plugins/external/core/sensible.js index 3ae2a632..55ba4e79 100644 --- a/plugins/sensible.js +++ b/plugins/external/core/sensible.js @@ -2,7 +2,7 @@ import fp from 'fastify-plugin' import sensible from '@fastify/sensible' /** - * This plugins adds some utilities to handle http errors + * This plugin adds some utilities to handle http errors * * @see https://github.com/fastify/fastify-sensible */ diff --git a/plugins/support.js b/plugins/support.js index a17d8b87..11d4b18b 100644 --- a/plugins/support.js +++ b/plugins/support.js @@ -1,8 +1,11 @@ import fp from 'fastify-plugin' -// the use of fastify-plugin is required to be able -// to export the decorators to the outer scope - +/** + * Tthe use of fastify-plugin is required to be able + * to export the decorators to the outer scope + * + * @see https://github.com/fastify/fastify-plugin + */ export default fp(async function (fastify, opts) { fastify.decorate('someSupport', function () { return 'hugs' From bbc21ac7f3a6f31035ba250fe84412845191bc3b Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 15:54:46 +0200 Subject: [PATCH 010/100] chore: add swagger --- app.js | 5 +++- package.json | 2 ++ routes/example/index.js | 2 +- routes/home.js | 2 +- swagger.js | 55 +++++++++++++++++++++++++++++++++++++++++ 5 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 swagger.js diff --git a/app.js b/app.js index e03573da..732d1369 100644 --- a/app.js +++ b/app.js @@ -1,12 +1,15 @@ /** - * If you would like to turn your application into a standalone executable, lookt at server.js file + * If you would like to turn your application into a standalone executable, look at server.js file * */ import path from 'node:path' import AutoLoad from '@fastify/autoload' +import swagger from './swagger.js' export default async function app (fastify, opts) { + fastify.register(swagger) + // This loads all plugins defined in plugins // those should be support plugins that are reused // through your application diff --git a/package.json b/package.json index 1306244e..526a9f13 100644 --- a/package.json +++ b/package.json @@ -23,6 +23,8 @@ "@fastify/env": "^4.3.0", "@fastify/helmet": "^11.1.1", "@fastify/sensible": "^5.0.0", + "@fastify/swagger": "^8.14.0", + "@fastify/swagger-ui": "^3.0.0", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", "fastify-plugin": "^4.0.0" diff --git a/routes/example/index.js b/routes/example/index.js index e0e17343..204f0d56 100644 --- a/routes/example/index.js +++ b/routes/example/index.js @@ -1,6 +1,6 @@ export default async function (fastify, opts) { // Prefixed with /example by the autoloader - fastify.get('/', async function (request, reply) { + fastify.get('/', { schema: { tags: ['Home'] } }, async function (request, reply) { return 'this is an example' }) } diff --git a/routes/home.js b/routes/home.js index 4c9ce5af..bc49c4f3 100644 --- a/routes/home.js +++ b/routes/home.js @@ -1,5 +1,5 @@ export default async function (fastify, opts) { - fastify.get('/', async function (request, reply) { + fastify.get('/', { schema: { tags: ['Home'] } }, async function (request, reply) { return 'Welcome to the official fastify demo!' }) } diff --git a/swagger.js b/swagger.js new file mode 100644 index 00000000..ca72e5c2 --- /dev/null +++ b/swagger.js @@ -0,0 +1,55 @@ +import fp from 'fastify-plugin' +import swagger from '@fastify/swagger' +import swaggerUi from '@fastify/swagger-ui' + +export default fp(async function (fastify, opts) { + /** + * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas + * + * @see https://github.com/fastify/fastify-swagger + */ + fastify.register(swagger, { + openapi: { + openapi: '3.0.0', + info: { + title: 'Fastify demo API', + description: 'The official Fastify demo API', + version: '0.1.0' + }, + components: { + securitySchemes: { + apiKey: { + type: 'apiKey', + name: 'apiKey', + in: 'header' + } + } + }, + externalDocs: { + url: 'https://swagger.io', + description: 'Find more info here' + } + } + }) + + /** + * A Fastify plugin for serving Swagger UI. + * + * @see https://github.com/fastify/fastify-swagger-ui + */ + await fastify.register(swaggerUi, { + routePrefix: '/documentation', + uiConfig: { + docExpansion: 'full', + deepLinking: false + }, + uiHooks: { + onRequest: function (request, reply, next) { next() }, + preHandler: function (request, reply, next) { next() } + }, + staticCSP: true, + transformStaticCSP: (header) => header, + transformSpecification: (swaggerObject, request, reply) => { return swaggerObject }, + transformSpecificationClone: true + }) +}) From fa32133c84a50df8df68affe31a50c8d86f04b26 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 15:58:04 +0200 Subject: [PATCH 011/100] fix: typo comment --- plugins/support.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/plugins/support.js b/plugins/support.js index 11d4b18b..ccd87edd 100644 --- a/plugins/support.js +++ b/plugins/support.js @@ -1,7 +1,7 @@ import fp from 'fastify-plugin' /** - * Tthe use of fastify-plugin is required to be able + * The use of fastify-plugin is required to be able * to export the decorators to the outer scope * * @see https://github.com/fastify/fastify-plugin From 5da24298fba7d044f0554e5522478824412c939e Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 16:11:53 +0200 Subject: [PATCH 012/100] fix: comment typo --- plugins/external/core/env.js | 1 - 1 file changed, 1 deletion(-) diff --git a/plugins/external/core/env.js b/plugins/external/core/env.js index 4cbc1cc2..45212722 100644 --- a/plugins/external/core/env.js +++ b/plugins/external/core/env.js @@ -4,7 +4,6 @@ import env from '@fastify/env' const schema = { type: 'object', required: ['PORT'], - // will read .env in root folder properties: { PORT: { type: 'string', From 1494030baf5b048408d92657035e8dafbb5e46f9 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 3 Jun 2024 16:17:51 +0200 Subject: [PATCH 013/100] fix: swagger tag Example in /example --- routes/example/index.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/routes/example/index.js b/routes/example/index.js index 204f0d56..c27c5ad7 100644 --- a/routes/example/index.js +++ b/routes/example/index.js @@ -1,6 +1,6 @@ export default async function (fastify, opts) { // Prefixed with /example by the autoloader - fastify.get('/', { schema: { tags: ['Home'] } }, async function (request, reply) { + fastify.get('/', { schema: { tags: ['Example'] } }, async function (request, reply) { return 'this is an example' }) } From 8ea43b35269bb8c42071226a5a85f06f94c2cbaf Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 07:43:57 +0200 Subject: [PATCH 014/100] refactor: simplify plugin structure folder --- plugins/{external/core => }/cors.js | 0 plugins/{external/core => }/env.js | 0 plugins/external/community/README.md | 1 - plugins/external/core/README.md | 1 - plugins/{external/core => }/helmet.js | 0 plugins/{external/core => }/sensible.js | 0 6 files changed, 2 deletions(-) rename plugins/{external/core => }/cors.js (100%) rename plugins/{external/core => }/env.js (100%) delete mode 100644 plugins/external/community/README.md delete mode 100644 plugins/external/core/README.md rename plugins/{external/core => }/helmet.js (100%) rename plugins/{external/core => }/sensible.js (100%) diff --git a/plugins/external/core/cors.js b/plugins/cors.js similarity index 100% rename from plugins/external/core/cors.js rename to plugins/cors.js diff --git a/plugins/external/core/env.js b/plugins/env.js similarity index 100% rename from plugins/external/core/env.js rename to plugins/env.js diff --git a/plugins/external/community/README.md b/plugins/external/community/README.md deleted file mode 100644 index 3936b30a..00000000 --- a/plugins/external/community/README.md +++ /dev/null @@ -1 +0,0 @@ -Fastify community plugins configuration. \ No newline at end of file diff --git a/plugins/external/core/README.md b/plugins/external/core/README.md deleted file mode 100644 index d5551c76..00000000 --- a/plugins/external/core/README.md +++ /dev/null @@ -1 +0,0 @@ -Fastify core plugins configuration. \ No newline at end of file diff --git a/plugins/external/core/helmet.js b/plugins/helmet.js similarity index 100% rename from plugins/external/core/helmet.js rename to plugins/helmet.js diff --git a/plugins/external/core/sensible.js b/plugins/sensible.js similarity index 100% rename from plugins/external/core/sensible.js rename to plugins/sensible.js From 1f899f72a175746771cca298b86f47c97b7063f9 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 08:12:47 +0200 Subject: [PATCH 015/100] refactor: put swagger registration in /plugins --- app.js | 9 +++----- plugins/swagger.js | 29 ++++++++++++++++++++++++ server.js | 5 +++-- swagger.js | 55 ---------------------------------------------- 4 files changed, 35 insertions(+), 63 deletions(-) create mode 100644 plugins/swagger.js delete mode 100644 swagger.js diff --git a/app.js b/app.js index 732d1369..42a28c33 100644 --- a/app.js +++ b/app.js @@ -4,23 +4,20 @@ */ import path from 'node:path' -import AutoLoad from '@fastify/autoload' -import swagger from './swagger.js' +import fastifyAutoload from '@fastify/autoload' export default async function app (fastify, opts) { - fastify.register(swagger) - // This loads all plugins defined in plugins // those should be support plugins that are reused // through your application - fastify.register(AutoLoad, { + fastify.register(fastifyAutoload, { dir: path.join(import.meta.dirname, 'plugins'), options: { ...opts } }) // This loads all plugins defined in routes // define your routes in one of these - fastify.register(AutoLoad, { + fastify.register(fastifyAutoload, { dir: path.join(import.meta.dirname, 'routes'), options: { ...opts } }) diff --git a/plugins/swagger.js b/plugins/swagger.js new file mode 100644 index 00000000..fb78fb66 --- /dev/null +++ b/plugins/swagger.js @@ -0,0 +1,29 @@ +import fp from 'fastify-plugin' +import fastifySwaggerUi from '@fastify/swagger-ui' +import fastifySwagger from '@fastify/swagger' + +export default fp(async function (fastify) { + /** + * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas + * + * @see https://github.com/fastify/fastify-swagger + */ + await fastify.register(fastifySwagger, { + openapi: { + info: { + title: 'Fastify demo API', + description: 'The official Fastify demo API', + version: '0.0.0' + } + } + }) + + /** + * A Fastify plugin for serving Swagger UI. + * + * @see https://github.com/fastify/fastify-swagger-ui + */ + await fastify.register(fastifySwaggerUi, { + // Set plugin options here + }) +}) diff --git a/server.js b/server.js index 82b5e86e..a4e7156c 100644 --- a/server.js +++ b/server.js @@ -20,11 +20,12 @@ async function init () { // Register your application as a normal plugin. app.register(appService) - // delay is the number of milliseconds for the graceful close to finish - const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY || 500 }, async function ({ signal, err, manual }) { + // Delay is the number of milliseconds for the graceful close to finish + const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY || 500 }, async ({ err }) => { if (err) { app.log.error(err) } + await app.close() }) diff --git a/swagger.js b/swagger.js deleted file mode 100644 index ca72e5c2..00000000 --- a/swagger.js +++ /dev/null @@ -1,55 +0,0 @@ -import fp from 'fastify-plugin' -import swagger from '@fastify/swagger' -import swaggerUi from '@fastify/swagger-ui' - -export default fp(async function (fastify, opts) { - /** - * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas - * - * @see https://github.com/fastify/fastify-swagger - */ - fastify.register(swagger, { - openapi: { - openapi: '3.0.0', - info: { - title: 'Fastify demo API', - description: 'The official Fastify demo API', - version: '0.1.0' - }, - components: { - securitySchemes: { - apiKey: { - type: 'apiKey', - name: 'apiKey', - in: 'header' - } - } - }, - externalDocs: { - url: 'https://swagger.io', - description: 'Find more info here' - } - } - }) - - /** - * A Fastify plugin for serving Swagger UI. - * - * @see https://github.com/fastify/fastify-swagger-ui - */ - await fastify.register(swaggerUi, { - routePrefix: '/documentation', - uiConfig: { - docExpansion: 'full', - deepLinking: false - }, - uiHooks: { - onRequest: function (request, reply, next) { next() }, - preHandler: function (request, reply, next) { next() } - }, - staticCSP: true, - transformStaticCSP: (header) => header, - transformSpecification: (swaggerObject, request, reply) => { return swaggerObject }, - transformSpecificationClone: true - }) -}) From 54807beead9ac51c8e81aa0cd02f34d20b967680 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 08:26:57 +0200 Subject: [PATCH 016/100] refactor: add pino-pretty to server.js --- package.json | 1 + server.js | 21 +++++++++++++++++++-- 2 files changed, 20 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 526a9f13..7ebefe12 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "test": "cross-env node --test test/**/*.test.js", "start": "fastify start -l info app.js", "dev": "fastify start -w -l info -P app.js", + "standalone": "node --env-file=.env server.js", "lint": "standard", "lint:fix": "npm run lint -- --fix" }, diff --git a/server.js b/server.js index a4e7156c..3e52049f 100644 --- a/server.js +++ b/server.js @@ -1,6 +1,8 @@ /** * This file is here only to show you how to proceed if you would * like to turn your application into a standalone executable. + * + * You can launch it with the command `npm run standalone` */ import Fastify from 'fastify' @@ -11,9 +13,24 @@ import closeWithGrace from 'close-with-grace' // Import your application as a normal plugin. import appService from './app.js' -// Instantiate Fastify with some config +const environment = process.env.NODE_ENV ?? 'production' +const envToLogger = { + development: { + level: 'info', + transport: { + target: 'pino-pretty', + options: { + translateTime: 'HH:MM:ss Z', + ignore: 'pid,hostname' + } + } + }, + production: true, + test: false +} + const app = Fastify({ - logger: true + logger: envToLogger[environment] ?? true }) async function init () { From 006ccd1aa23e4775d59baf990b28a1902f550e33 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 09:08:45 +0200 Subject: [PATCH 017/100] chore: add error handlers --- app.js | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/app.js b/app.js index 42a28c33..a862792e 100644 --- a/app.js +++ b/app.js @@ -21,4 +21,35 @@ export default async function app (fastify, opts) { dir: path.join(import.meta.dirname, 'routes'), options: { ...opts } }) + + fastify.setErrorHandler((err, request, reply) => { + request.log.error({ + err, + request: { + method: request.method, + url: request.url, + query: request.query, + params: request.params + } + }, 'Unhandled error occurred') + + reply.code(err.statusCode ?? 500) + + return { message: 'Internal Server Error' } + }) + + fastify.setNotFoundHandler(async (request, reply) => { + request.log.warn({ + request: { + method: request.method, + url: request.url, + query: request.query, + params: request.params + } + }, 'Resource not found') + + reply.code(404) + + return { message: 'Not Found' } + }) } From 5ed556fe1f70a42baa4160df4895796351c28481 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 09:11:00 +0200 Subject: [PATCH 018/100] refactor: unecessary await setNotFoundHandler --- app.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/app.js b/app.js index a862792e..5d9c682f 100644 --- a/app.js +++ b/app.js @@ -38,7 +38,7 @@ export default async function app (fastify, opts) { return { message: 'Internal Server Error' } }) - fastify.setNotFoundHandler(async (request, reply) => { + fastify.setNotFoundHandler((request, reply) => { request.log.warn({ request: { method: request.method, From d48b86cef1c52c6bb1e8da117e82c21ec71da396 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 09:24:25 +0200 Subject: [PATCH 019/100] test: root not found handler --- test/routes/error-handler.test.js | 1 + test/routes/not-found-handler.test.js | 15 +++++++++++++++ 2 files changed, 16 insertions(+) create mode 100644 test/routes/error-handler.test.js create mode 100644 test/routes/not-found-handler.test.js diff --git a/test/routes/error-handler.test.js b/test/routes/error-handler.test.js new file mode 100644 index 00000000..1509633c --- /dev/null +++ b/test/routes/error-handler.test.js @@ -0,0 +1 @@ +// TODO: See fastify-cli to add route throwing and error before ready is called \ No newline at end of file diff --git a/test/routes/not-found-handler.test.js b/test/routes/not-found-handler.test.js new file mode 100644 index 00000000..e3957f07 --- /dev/null +++ b/test/routes/not-found-handler.test.js @@ -0,0 +1,15 @@ +import { test } from 'node:test' +import assert from 'node:assert' +import { build } from '../helper.js' + +test('root not found handler', async (t) => { + const app = await build(t); + + const res = await app.inject({ + method: 'GET', + url: '/this-route-does-not-exist' + }); + + assert.strictEqual(res.statusCode, 404); + assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Not Found' }); +}); From d8101c49f39e0b7c23d877e4ef9a4ad75a037996 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:11:41 +0200 Subject: [PATCH 020/100] chore: reuse existing workflow --- .github/workflows/ci.yml | 44 ++++++++++++++++------------------------ 1 file changed, 17 insertions(+), 27 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7a1316ff..df1ac8df 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,31 +1,21 @@ -name: Fastify Official Demo CI - +name: CI on: push: - branches: [ "main" ] + branches: + - main + - master + - next + - 'v*' + paths-ignore: + - 'docs/**' + - '*.md' pull_request: - branches: [ "main" ] - + paths-ignore: + - 'docs/**' + - '*.md' jobs: - build: - runs-on: ${{ matrix.os }} - strategy: - matrix: - os: [ubuntu-latest, windows-latest, macos-latest] - node-version: [22] - - steps: - - uses: actions/checkout@v3 - - name: Use Node.js ${{ matrix.node-version }} - uses: actions/setup-node@v3 - with: - node-version: ${{ matrix.node-version }} - - - name: Install dependencies - run: npm i --ignore-scripts - - - name: Lint Code - run: npm run lint - - - name: Test - run: npm test + test: + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + with: + license-check: true + lint: true \ No newline at end of file From d41849bf083dc695e56db9fd71a46e678230d170 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:17:19 +0200 Subject: [PATCH 021/100] test on v4 --- .github/workflows/ci.yml | 2 +- test/routes/error-handler.test.js | 2 +- test/routes/not-found-handler.test.js | 10 +++++----- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df1ac8df..774e33d8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ on: - '*.md' jobs: test: - uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v4 with: license-check: true lint: true \ No newline at end of file diff --git a/test/routes/error-handler.test.js b/test/routes/error-handler.test.js index 1509633c..d012b00e 100644 --- a/test/routes/error-handler.test.js +++ b/test/routes/error-handler.test.js @@ -1 +1 @@ -// TODO: See fastify-cli to add route throwing and error before ready is called \ No newline at end of file +// TODO: See fastify-cli to add route throwing and error before ready is called diff --git a/test/routes/not-found-handler.test.js b/test/routes/not-found-handler.test.js index e3957f07..e073fa0b 100644 --- a/test/routes/not-found-handler.test.js +++ b/test/routes/not-found-handler.test.js @@ -3,13 +3,13 @@ import assert from 'node:assert' import { build } from '../helper.js' test('root not found handler', async (t) => { - const app = await build(t); + const app = await build(t) const res = await app.inject({ method: 'GET', url: '/this-route-does-not-exist' - }); + }) - assert.strictEqual(res.statusCode, 404); - assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Not Found' }); -}); + assert.strictEqual(res.statusCode, 404) + assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Not Found' }) +}) From 84bf9f931509ac48375eeae293a9dd6940b4418f Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:20:23 +0200 Subject: [PATCH 022/100] chore: use ci v4 --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index df1ac8df..f9523f69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ on: - '*.md' jobs: test: - uses: fastify/workflows/.github/workflows/plugins-ci.yml@v3 + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v4.1.0 with: license-check: true lint: true \ No newline at end of file From b4981ed1582ed8298880d40922a068b358735399 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:22:06 +0200 Subject: [PATCH 023/100] fix: run lint fix --- test/routes/error-handler.test.js | 2 +- test/routes/not-found-handler.test.js | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/test/routes/error-handler.test.js b/test/routes/error-handler.test.js index 1509633c..d012b00e 100644 --- a/test/routes/error-handler.test.js +++ b/test/routes/error-handler.test.js @@ -1 +1 @@ -// TODO: See fastify-cli to add route throwing and error before ready is called \ No newline at end of file +// TODO: See fastify-cli to add route throwing and error before ready is called diff --git a/test/routes/not-found-handler.test.js b/test/routes/not-found-handler.test.js index e3957f07..e073fa0b 100644 --- a/test/routes/not-found-handler.test.js +++ b/test/routes/not-found-handler.test.js @@ -3,13 +3,13 @@ import assert from 'node:assert' import { build } from '../helper.js' test('root not found handler', async (t) => { - const app = await build(t); + const app = await build(t) const res = await app.inject({ method: 'GET', url: '/this-route-does-not-exist' - }); + }) - assert.strictEqual(res.statusCode, 404); - assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Not Found' }); -}); + assert.strictEqual(res.statusCode, 404) + assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Not Found' }) +}) From 05ffbab49d78edaea53ed556300b518d617b1174 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:31:27 +0200 Subject: [PATCH 024/100] fix: cant use import.meta.dirname for now --- app.js | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/app.js b/app.js index 5d9c682f..b8d5e5c5 100644 --- a/app.js +++ b/app.js @@ -5,20 +5,25 @@ import path from 'node:path' import fastifyAutoload from '@fastify/autoload' +import { fileURLToPath } from 'url' +import { dirname } from 'path' + +const __filename = fileURLToPath(import.meta.url) +const __dirname = dirname(__filename) export default async function app (fastify, opts) { // This loads all plugins defined in plugins // those should be support plugins that are reused // through your application fastify.register(fastifyAutoload, { - dir: path.join(import.meta.dirname, 'plugins'), + dir: path.join(__dirname, 'plugins'), options: { ...opts } }) // This loads all plugins defined in routes // define your routes in one of these fastify.register(fastifyAutoload, { - dir: path.join(import.meta.dirname, 'routes'), + dir: path.join(__dirname, 'routes'), options: { ...opts } }) From 3d580756d5837404617dab770f1bfda71edc877d Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:38:22 +0200 Subject: [PATCH 025/100] update readme getting-started --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index ec56dea4..2ffe6e17 100644 --- a/README.md +++ b/README.md @@ -10,9 +10,9 @@ The aim of this repository is to provide a concrete example of a Fastify applica **Prerequisites:** You need to have Node.js version 22 or higher installed. ## Getting started -Install [`fastify-cli`](https://github.com/fastify/fastify-cli): -``` -npm install fastify-cli --global +Install the dependencies: +```bash +npm install ``` ## Available Scripts From 31d0e15fd701a8695704883e95d047a5d11cb138 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:44:22 +0200 Subject: [PATCH 026/100] fix: node imports --- app.js | 7 +++---- test/helper.js | 6 +++++- 2 files changed, 8 insertions(+), 5 deletions(-) diff --git a/app.js b/app.js index b8d5e5c5..278390dc 100644 --- a/app.js +++ b/app.js @@ -3,13 +3,12 @@ * */ -import path from 'node:path' import fastifyAutoload from '@fastify/autoload' -import { fileURLToPath } from 'url' -import { dirname } from 'path' +import { fileURLToPath } from 'node:url' +import path from 'node:path' const __filename = fileURLToPath(import.meta.url) -const __dirname = dirname(__filename) +const __dirname = path.dirname(__filename) export default async function app (fastify, opts) { // This loads all plugins defined in plugins diff --git a/test/helper.js b/test/helper.js index ee5aff55..5a4026e2 100644 --- a/test/helper.js +++ b/test/helper.js @@ -3,8 +3,12 @@ import { build as buildApplication } from 'fastify-cli/helper.js' import path from 'node:path' +import { fileURLToPath } from 'node:url' -const AppPath = path.join(import.meta.dirname, '../app.js') +const __filename = fileURLToPath(import.meta.url) +const __dirname = path.dirname(__filename) + +const AppPath = path.join(__dirname, '../app.js') // Fill in this config with all the configurations // needed for testing the application From 6653a37ebb2f055aaaba7990fcee141806786af3 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:52:46 +0200 Subject: [PATCH 027/100] chore: use glob to launch tests --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7ebefe12..75ffb45a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "test" }, "scripts": { - "test": "cross-env node --test test/**/*.test.js", + "test": "glob -c \"node --test\" \"./test/**/*.test.js\"", "start": "fastify start -l info app.js", "dev": "fastify start -w -l info -P app.js", "standalone": "node --env-file=.env server.js", @@ -31,7 +31,7 @@ "fastify-plugin": "^4.0.0" }, "devDependencies": { - "cross-env": "^7.0.3", + "glob": "^10.4.1", "standard": "^17.1.0" } } From 1baf9b7a7fc532dc4cd132bd17d7f6637c0ddc4d Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:56:29 +0200 Subject: [PATCH 028/100] fix: invalid workflow file --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 774e33d8..f9523f69 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ on: - '*.md' jobs: test: - uses: fastify/workflows/.github/workflows/plugins-ci.yml@v4 + uses: fastify/workflows/.github/workflows/plugins-ci.yml@v4.1.0 with: license-check: true lint: true \ No newline at end of file From 4cbf9c2a0e5546756335e81657cc298eef092468 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 10:58:46 +0200 Subject: [PATCH 029/100] chore: use glob to run test suite --- package.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/package.json b/package.json index 7ebefe12..75ffb45a 100644 --- a/package.json +++ b/package.json @@ -8,7 +8,7 @@ "test": "test" }, "scripts": { - "test": "cross-env node --test test/**/*.test.js", + "test": "glob -c \"node --test\" \"./test/**/*.test.js\"", "start": "fastify start -l info app.js", "dev": "fastify start -w -l info -P app.js", "standalone": "node --env-file=.env server.js", @@ -31,7 +31,7 @@ "fastify-plugin": "^4.0.0" }, "devDependencies": { - "cross-env": "^7.0.3", + "glob": "^10.4.1", "standard": "^17.1.0" } } From 10e85713d1c43dbcc461165d5e73dd8b13317a68 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 15:19:55 +0200 Subject: [PATCH 030/100] chore: use typescript --- @types/fastify/index.d.ts | 11 ++++++++++ @types/node/environment.d.ts | 11 ++++++++++ app.js => app.ts | 6 ++--- package.json | 21 ++++++++++++------ plugins/{cors.js => cors.ts} | 0 plugins/{env.js => env.ts} | 0 plugins/{helmet.js => helmet.ts} | 0 plugins/{sensible.js => sensible.ts} | 2 +- plugins/{support.js => support.ts} | 2 +- plugins/{swagger.js => swagger.ts} | 0 routes/example/index.js | 6 ----- routes/example/index.ts | 19 ++++++++++++++++ routes/home.js | 5 ----- routes/home.ts | 18 +++++++++++++++ server.js => server.ts | 22 +++++++++++++------ test/{helper.js => helper.ts} | 3 ++- .../{support.test.js => support.test.ts} | 2 +- ...-handler.test.js => error-handler.test.ts} | 0 .../{example.test.js => example.test.ts} | 2 +- test/routes/{home.test.js => home.test.ts} | 3 ++- ...dler.test.js => not-found-handler.test.ts} | 0 tsconfig.json | 6 +++++ 22 files changed, 105 insertions(+), 34 deletions(-) create mode 100644 @types/fastify/index.d.ts create mode 100644 @types/node/environment.d.ts rename app.js => app.ts (89%) rename plugins/{cors.js => cors.ts} (100%) rename plugins/{env.js => env.ts} (100%) rename plugins/{helmet.js => helmet.ts} (100%) rename plugins/{sensible.js => sensible.ts} (90%) rename plugins/{support.js => support.ts} (83%) rename plugins/{swagger.js => swagger.ts} (100%) delete mode 100644 routes/example/index.js create mode 100644 routes/example/index.ts delete mode 100644 routes/home.js create mode 100644 routes/home.ts rename server.js => server.ts (71%) rename test/{helper.js => helper.ts} (91%) rename test/plugins/{support.test.js => support.test.ts} (92%) rename test/routes/{error-handler.test.js => error-handler.test.ts} (100%) rename test/routes/{example.test.js => example.test.ts} (73%) rename test/routes/{home.test.js => home.test.ts} (68%) rename test/routes/{not-found-handler.test.js => not-found-handler.test.ts} (100%) create mode 100644 tsconfig.json diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts new file mode 100644 index 00000000..fc651ce0 --- /dev/null +++ b/@types/fastify/index.d.ts @@ -0,0 +1,11 @@ +declare module 'fastify' { + export interface FastifyInstance< + HttpServer = http.Server, + HttpRequest = http.IncomingMessage, + HttpResponse = http.ServerResponse + > { + someSupport(): void + } +} + +export {} diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts new file mode 100644 index 00000000..7c6abe92 --- /dev/null +++ b/@types/node/environment.d.ts @@ -0,0 +1,11 @@ +declare global { + namespace NodeJS { + interface ProcessEnv { + NODE_ENV?: 'development' | 'production' + PORT?: number + FASTIFY_CLOSE_GRACE_DELAY?: number + } + } +} + +export {} diff --git a/app.js b/app.ts similarity index 89% rename from app.js rename to app.ts index 278390dc..c93c899b 100644 --- a/app.js +++ b/app.ts @@ -1,16 +1,16 @@ /** * If you would like to turn your application into a standalone executable, look at server.js file - * - */ +*/ import fastifyAutoload from '@fastify/autoload' import { fileURLToPath } from 'node:url' import path from 'node:path' +import { FastifyInstance, FastifyPluginOptions } from 'fastify' const __filename = fileURLToPath(import.meta.url) const __dirname = path.dirname(__filename) -export default async function app (fastify, opts) { +export default async function serviceApp (fastify: FastifyInstance, opts: FastifyPluginOptions) { // This loads all plugins defined in plugins // those should be support plugins that are reused // through your application diff --git a/package.json b/package.json index 75ffb45a..1203b33e 100644 --- a/package.json +++ b/package.json @@ -8,12 +8,14 @@ "test": "test" }, "scripts": { - "test": "glob -c \"node --test\" \"./test/**/*.test.js\"", - "start": "fastify start -l info app.js", - "dev": "fastify start -w -l info -P app.js", - "standalone": "node --env-file=.env server.js", - "lint": "standard", - "lint:fix": "npm run lint -- --fix" + "build": "rm -rf dist && tsc", + "watch": "npm run build -- --watch", + "test": "npm run build && glob -c \"node --test\" \"./test/**/*.dist.js\"", + "start": "npm run build && fastify start -l info dist/app.js", + "dev": "npm run build && fastify start -w -l info -P dist/app.js", + "standalone": "npm run build && node --env-file=.env dist/server.js", + "lint": "echo \"should lint\"", + "lint:fix": "echo \"should lint:fix\"" }, "keywords": [], "author": "Michelet Jean ", @@ -26,12 +28,17 @@ "@fastify/sensible": "^5.0.0", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^3.0.0", + "@fastify/type-provider-typebox": "^4.0.0", + "@sinclair/typebox": "^0.32.31", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", "fastify-plugin": "^4.0.0" }, "devDependencies": { + "@types/node": "^20.14.2", + "fastify-tsconfig": "^2.0.0", "glob": "^10.4.1", - "standard": "^17.1.0" + "ts-standard": "^12.0.2", + "typescript": "^5.4.5" } } diff --git a/plugins/cors.js b/plugins/cors.ts similarity index 100% rename from plugins/cors.js rename to plugins/cors.ts diff --git a/plugins/env.js b/plugins/env.ts similarity index 100% rename from plugins/env.js rename to plugins/env.ts diff --git a/plugins/helmet.js b/plugins/helmet.ts similarity index 100% rename from plugins/helmet.js rename to plugins/helmet.ts diff --git a/plugins/sensible.js b/plugins/sensible.ts similarity index 90% rename from plugins/sensible.js rename to plugins/sensible.ts index 55ba4e79..6b79d1d6 100644 --- a/plugins/sensible.js +++ b/plugins/sensible.ts @@ -8,6 +8,6 @@ import sensible from '@fastify/sensible' */ export default fp(async function (fastify, opts) { fastify.register(sensible, { - errorHandler: false + // Set plugin options here }) }) diff --git a/plugins/support.js b/plugins/support.ts similarity index 83% rename from plugins/support.js rename to plugins/support.ts index ccd87edd..845d7569 100644 --- a/plugins/support.js +++ b/plugins/support.ts @@ -6,7 +6,7 @@ import fp from 'fastify-plugin' * * @see https://github.com/fastify/fastify-plugin */ -export default fp(async function (fastify, opts) { +export default fp(async function (fastify) { fastify.decorate('someSupport', function () { return 'hugs' }) diff --git a/plugins/swagger.js b/plugins/swagger.ts similarity index 100% rename from plugins/swagger.js rename to plugins/swagger.ts diff --git a/routes/example/index.js b/routes/example/index.js deleted file mode 100644 index c27c5ad7..00000000 --- a/routes/example/index.js +++ /dev/null @@ -1,6 +0,0 @@ -export default async function (fastify, opts) { - // Prefixed with /example by the autoloader - fastify.get('/', { schema: { tags: ['Example'] } }, async function (request, reply) { - return 'this is an example' - }) -} diff --git a/routes/example/index.ts b/routes/example/index.ts new file mode 100644 index 00000000..c75e37ec --- /dev/null +++ b/routes/example/index.ts @@ -0,0 +1,19 @@ +import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' + +const plugin: FastifyPluginAsyncTypebox = async (fastify) => { + // Prefixed with /example by the autoloader + fastify.get('/', { + schema: { + response: { + 200: Type.Object({ + message: Type.String() + }) + }, + tags: ['Example'] + } + }, async function (request, reply) { + return { message: 'This is an example' } + }) +} + +export default plugin diff --git a/routes/home.js b/routes/home.js deleted file mode 100644 index bc49c4f3..00000000 --- a/routes/home.js +++ /dev/null @@ -1,5 +0,0 @@ -export default async function (fastify, opts) { - fastify.get('/', { schema: { tags: ['Home'] } }, async function (request, reply) { - return 'Welcome to the official fastify demo!' - }) -} diff --git a/routes/home.ts b/routes/home.ts new file mode 100644 index 00000000..72c92146 --- /dev/null +++ b/routes/home.ts @@ -0,0 +1,18 @@ +import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' + +const plugin: FastifyPluginAsyncTypebox = async (fastify) => { + fastify.get('/', { + schema: { + response: { + 200: Type.Object({ + message: Type.String() + }) + }, + tags: ['Home'] + } + }, async function (request, reply) { + return { message: 'Welcome to the official fastify demo!' } + }) +} + +export default plugin diff --git a/server.js b/server.ts similarity index 71% rename from server.js rename to server.ts index 3e52049f..584d171e 100644 --- a/server.js +++ b/server.ts @@ -11,7 +11,7 @@ import Fastify from 'fastify' import closeWithGrace from 'close-with-grace' // Import your application as a normal plugin. -import appService from './app.js' +import serviceApp from './app.js' const environment = process.env.NODE_ENV ?? 'production' const envToLogger = { @@ -30,16 +30,24 @@ const envToLogger = { } const app = Fastify({ - logger: envToLogger[environment] ?? true + logger: envToLogger[environment] ?? true, + ajv: { + customOptions: { + coerceTypes: 'array', // change data type of data to match type keyword + removeAdditional: 'all',// Remove additional body properties + }, + }, }) async function init () { // Register your application as a normal plugin. - app.register(appService) + app.register(serviceApp) + + // console.log(app.config("hello")) // Delay is the number of milliseconds for the graceful close to finish - const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY || 500 }, async ({ err }) => { - if (err) { + const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 500 }, async ({ err }) => { + if (err != null) { app.log.error(err) } @@ -54,8 +62,8 @@ async function init () { await app.ready() // Start listening. - app.listen({ port: process.env.PORT || 3000 }, (err) => { - if (err) { + app.listen({ port: process.env.PORT ?? 3000 }, (err) => { + if (err != null) { app.log.error(err) process.exit(1) } diff --git a/test/helper.js b/test/helper.ts similarity index 91% rename from test/helper.js rename to test/helper.ts index 5a4026e2..140b5932 100644 --- a/test/helper.js +++ b/test/helper.ts @@ -3,6 +3,7 @@ import { build as buildApplication } from 'fastify-cli/helper.js' import path from 'node:path' +import { TestContext } from 'node:test' import { fileURLToPath } from 'node:url' const __filename = fileURLToPath(import.meta.url) @@ -17,7 +18,7 @@ export function config () { } // automatically build and tear down our instance -export async function build (t) { +export async function build (t: TestContext) { // you can set all the options supported by the fastify CLI command const argv = [AppPath] diff --git a/test/plugins/support.test.js b/test/plugins/support.test.ts similarity index 92% rename from test/plugins/support.test.js rename to test/plugins/support.test.ts index 1de4e2ed..44da2667 100644 --- a/test/plugins/support.test.js +++ b/test/plugins/support.test.ts @@ -4,7 +4,7 @@ import assert from 'node:assert' import Fastify from 'fastify' import Support from '../../plugins/support.js' -test('support works standalone', async (t) => { +test('support works standalone', async () => { const fastify = Fastify() fastify.register(Support) diff --git a/test/routes/error-handler.test.js b/test/routes/error-handler.test.ts similarity index 100% rename from test/routes/error-handler.test.js rename to test/routes/error-handler.test.ts diff --git a/test/routes/example.test.js b/test/routes/example.test.ts similarity index 73% rename from test/routes/example.test.js rename to test/routes/example.test.ts index bd4b7232..f8b83844 100644 --- a/test/routes/example.test.js +++ b/test/routes/example.test.ts @@ -9,5 +9,5 @@ test('example is loaded', async (t) => { url: '/example' }) - assert.equal(res.payload, 'this is an example') + assert.deepStrictEqual(JSON.parse(res.payload), { message: 'This is an example' }) }) diff --git a/test/routes/home.test.js b/test/routes/home.test.ts similarity index 68% rename from test/routes/home.test.js rename to test/routes/home.test.ts index 47d46c6b..36b34a0c 100644 --- a/test/routes/home.test.js +++ b/test/routes/home.test.ts @@ -8,5 +8,6 @@ test('default root route', async (t) => { const res = await app.inject({ url: '/' }) - assert.deepStrictEqual(res.payload, 'Welcome to the official fastify demo!') + + assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Welcome to the official fastify demo!' }) }) diff --git a/test/routes/not-found-handler.test.js b/test/routes/not-found-handler.test.ts similarity index 100% rename from test/routes/not-found-handler.test.js rename to test/routes/not-found-handler.test.ts diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 00000000..868a7c0a --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,6 @@ +{ + "extends": "fastify-tsconfig", + "compilerOptions": { + "outDir": "dist", + } +} \ No newline at end of file From 7d0829e64c0bad64465e2fc9e97022d31b710458 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 18:38:19 +0200 Subject: [PATCH 031/100] chore: use typescript-eslint with prettier --- .github/workflows/ci.yml | 18 +++--- .prettierignore | 1 + @types/fastify/index.d.ts | 12 ++-- @types/node/environment.d.ts | 8 +-- README.md | 9 ++- app.ts | 87 +++++++++++++++------------ eslint.config.js | 9 +++ package.json | 13 ++-- plugins/README.md | 6 +- plugins/cors.ts | 10 +-- plugins/env.ts | 30 ++++----- plugins/helmet.ts | 10 +-- plugins/sensible.ts | 10 +-- plugins/support.ts | 10 +-- plugins/swagger.ts | 38 ++++++------ routes/example/index.ts | 35 ++++++----- routes/home.ts | 35 ++++++----- server.ts | 67 +++++++++++---------- test/helper.ts | 28 ++++----- test/plugins/support.test.ts | 20 +++--- test/routes/example.test.ts | 20 +++--- test/routes/home.test.ts | 20 +++--- test/routes/not-found-handler.test.ts | 22 +++---- tsconfig.json | 10 +-- 24 files changed, 284 insertions(+), 244 deletions(-) create mode 100644 .prettierignore create mode 100644 eslint.config.js diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f9523f69..41f29272 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -2,20 +2,20 @@ name: CI on: push: branches: - - main - - master - - next - - 'v*' + - main + - master + - next + - "v*" paths-ignore: - - 'docs/**' - - '*.md' + - "docs/**" + - "*.md" pull_request: paths-ignore: - - 'docs/**' - - '*.md' + - "docs/**" + - "*.md" jobs: test: uses: fastify/workflows/.github/workflows/plugins-ci.yml@v4.1.0 with: license-check: true - lint: true \ No newline at end of file + lint: true diff --git a/.prettierignore b/.prettierignore new file mode 100644 index 00000000..53c37a16 --- /dev/null +++ b/.prettierignore @@ -0,0 +1 @@ +dist \ No newline at end of file diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts index fc651ce0..9f5ec4a2 100644 --- a/@types/fastify/index.d.ts +++ b/@types/fastify/index.d.ts @@ -1,11 +1,7 @@ -declare module 'fastify' { - export interface FastifyInstance< - HttpServer = http.Server, - HttpRequest = http.IncomingMessage, - HttpResponse = http.ServerResponse - > { - someSupport(): void +declare module "fastify" { + export interface FastifyInstance { + someSupport(): void; } } -export {} +export {}; diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index 7c6abe92..764f30ca 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -1,11 +1,11 @@ declare global { namespace NodeJS { interface ProcessEnv { - NODE_ENV?: 'development' | 'production' - PORT?: number - FASTIFY_CLOSE_GRACE_DELAY?: number + NODE_ENV?: "development" | "production"; + PORT?: number; + FASTIFY_CLOSE_GRACE_DELAY?: number; } } } -export {} +export {}; diff --git a/README.md b/README.md index 2ffe6e17..39f1fe86 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# Fastify Official Demo +# Fastify Official Demo ![CI](https://github.com/fastify/demo/workflows/CI/badge.svg) [![js-standard-style](https://img.shields.io/badge/code%20style-standard-brightgreen.svg?style=flat)](https://standardjs.com/) @@ -10,12 +10,15 @@ The aim of this repository is to provide a concrete example of a Fastify applica **Prerequisites:** You need to have Node.js version 22 or higher installed. ## Getting started + Install the dependencies: + ```bash npm install ``` ## Available Scripts + In the project directory, you can run: ### `npm run dev` @@ -34,7 +37,3 @@ Run the test cases. ## Learn More To learn Fastify, check out the [Fastify documentation](https://fastify.dev/docs/latest/). - - - - diff --git a/app.ts b/app.ts index c93c899b..1f08012c 100644 --- a/app.ts +++ b/app.ts @@ -1,59 +1,68 @@ /** * If you would like to turn your application into a standalone executable, look at server.js file -*/ + */ -import fastifyAutoload from '@fastify/autoload' -import { fileURLToPath } from 'node:url' -import path from 'node:path' -import { FastifyInstance, FastifyPluginOptions } from 'fastify' +import fastifyAutoload from "@fastify/autoload"; +import { fileURLToPath } from "node:url"; +import path from "node:path"; +import { FastifyInstance, FastifyPluginOptions } from "fastify"; -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -export default async function serviceApp (fastify: FastifyInstance, opts: FastifyPluginOptions) { +export default async function serviceApp( + fastify: FastifyInstance, + opts: FastifyPluginOptions, +) { // This loads all plugins defined in plugins // those should be support plugins that are reused // through your application fastify.register(fastifyAutoload, { - dir: path.join(__dirname, 'plugins'), - options: { ...opts } - }) + dir: path.join(__dirname, "plugins"), + options: { ...opts }, + }); // This loads all plugins defined in routes // define your routes in one of these fastify.register(fastifyAutoload, { - dir: path.join(__dirname, 'routes'), - options: { ...opts } - }) + dir: path.join(__dirname, "routes"), + options: { ...opts }, + }); fastify.setErrorHandler((err, request, reply) => { - request.log.error({ - err, - request: { - method: request.method, - url: request.url, - query: request.query, - params: request.params - } - }, 'Unhandled error occurred') + request.log.error( + { + err, + request: { + method: request.method, + url: request.url, + query: request.query, + params: request.params, + }, + }, + "Unhandled error occurred", + ); - reply.code(err.statusCode ?? 500) + reply.code(err.statusCode ?? 500); - return { message: 'Internal Server Error' } - }) + return { message: "Internal Server Error" }; + }); fastify.setNotFoundHandler((request, reply) => { - request.log.warn({ - request: { - method: request.method, - url: request.url, - query: request.query, - params: request.params - } - }, 'Resource not found') - - reply.code(404) - - return { message: 'Not Found' } - }) + request.log.warn( + { + request: { + method: request.method, + url: request.url, + query: request.query, + params: request.params, + }, + }, + "Resource not found", + ); + + reply.code(404); + + return { message: "Not Found" }; + }); } diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..1eb1b43f --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,9 @@ +// @ts-check + +import eslint from "@eslint/js"; +import tseslint from "typescript-eslint"; + +export default tseslint.config( + eslint.configs.recommended, + ...tseslint.configs.recommended, +); diff --git a/package.json b/package.json index 1203b33e..2b04fcf5 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "start": "npm run build && fastify start -l info dist/app.js", "dev": "npm run build && fastify start -w -l info -P dist/app.js", "standalone": "npm run build && node --env-file=.env dist/server.js", - "lint": "echo \"should lint\"", - "lint:fix": "echo \"should lint:fix\"" + "lint": "prettier . --list-different && eslint . --ignore-pattern=dist", + "prettier": "prettier . --write" }, "keywords": [], "author": "Michelet Jean ", @@ -32,13 +32,18 @@ "@sinclair/typebox": "^0.32.31", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", - "fastify-plugin": "^4.0.0" + "fastify-plugin": "^4.0.0", + "prettier": "^3.3.1" }, "devDependencies": { + "@eslint/js": "^9.4.0", + "@types/eslint__js": "^8.42.3", "@types/node": "^20.14.2", + "eslint": "^8.57.0", "fastify-tsconfig": "^2.0.0", "glob": "^10.4.1", "ts-standard": "^12.0.2", - "typescript": "^5.4.5" + "typescript": "^5.4.5", + "typescript-eslint": "^7.12.0" } } diff --git a/plugins/README.md b/plugins/README.md index 76b414d6..fc72d0b1 100644 --- a/plugins/README.md +++ b/plugins/README.md @@ -8,6 +8,6 @@ Files in this folder are typically defined through the Check out: -* [The hitchhiker's guide to plugins](https://fastify.dev/docs/latest/Guides/Plugins-Guide/) -* [Fastify decorators](https://fastify.dev/docs/latest/Reference/Decorators/). -* [Fastify lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/). +- [The hitchhiker's guide to plugins](https://fastify.dev/docs/latest/Guides/Plugins-Guide/) +- [Fastify decorators](https://fastify.dev/docs/latest/Reference/Decorators/). +- [Fastify lifecycle](https://fastify.dev/docs/latest/Reference/Lifecycle/). diff --git a/plugins/cors.ts b/plugins/cors.ts index f1b32334..f19d93fa 100644 --- a/plugins/cors.ts +++ b/plugins/cors.ts @@ -1,13 +1,13 @@ -import fp from 'fastify-plugin' -import cors from '@fastify/cors' +import fp from "fastify-plugin"; +import cors from "@fastify/cors"; /** * This plugins enables the use of CORS. * * @see https://github.com/fastify/fastify-cors */ -export default fp(async function (fastify, opts) { +export default fp(async function (fastify) { fastify.register(cors, { // Set plugin options here - }) -}) + }); +}); diff --git a/plugins/env.ts b/plugins/env.ts index 45212722..7e204385 100644 --- a/plugins/env.ts +++ b/plugins/env.ts @@ -1,21 +1,21 @@ -import fp from 'fastify-plugin' -import env from '@fastify/env' +import fp from "fastify-plugin"; +import env from "@fastify/env"; const schema = { - type: 'object', - required: ['PORT'], + type: "object", + required: ["PORT"], properties: { PORT: { - type: 'string', - default: 3000 - } - } -} + type: "string", + default: 3000, + }, + }, +}; const options = { // Decorate Fastify instance with `config` key // Optional, default: 'config' - confKey: 'config', + confKey: "config", // Schema to validate schema, @@ -30,14 +30,14 @@ const options = { // Source for the configuration data // Optional, default: process.env - data: process.env -} + data: process.env, +}; /** * This plugins helps to check environment variables. * * @see https://github.com/fastify/fastify-env */ -export default fp(async function (fastify, opts) { - fastify.register(env, options) -}) +export default fp(async function (fastify) { + fastify.register(env, options); +}); diff --git a/plugins/helmet.ts b/plugins/helmet.ts index a0b6a86c..c63e06f9 100644 --- a/plugins/helmet.ts +++ b/plugins/helmet.ts @@ -1,13 +1,13 @@ -import fp from 'fastify-plugin' -import helmet from '@fastify/helmet' +import fp from "fastify-plugin"; +import helmet from "@fastify/helmet"; /** * This plugins sets the basic security headers. * * @see https://github.com/fastify/fastify-helmet */ -export default fp(async function (fastify, opts) { +export default fp(async function (fastify) { fastify.register(helmet, { // Set plugin options here - }) -}) + }); +}); diff --git a/plugins/sensible.ts b/plugins/sensible.ts index 6b79d1d6..439ae5f3 100644 --- a/plugins/sensible.ts +++ b/plugins/sensible.ts @@ -1,13 +1,13 @@ -import fp from 'fastify-plugin' -import sensible from '@fastify/sensible' +import fp from "fastify-plugin"; +import sensible from "@fastify/sensible"; /** * This plugin adds some utilities to handle http errors * * @see https://github.com/fastify/fastify-sensible */ -export default fp(async function (fastify, opts) { +export default fp(async function (fastify) { fastify.register(sensible, { // Set plugin options here - }) -}) + }); +}); diff --git a/plugins/support.ts b/plugins/support.ts index 845d7569..55d8fcd0 100644 --- a/plugins/support.ts +++ b/plugins/support.ts @@ -1,4 +1,4 @@ -import fp from 'fastify-plugin' +import fp from "fastify-plugin"; /** * The use of fastify-plugin is required to be able @@ -7,7 +7,7 @@ import fp from 'fastify-plugin' * @see https://github.com/fastify/fastify-plugin */ export default fp(async function (fastify) { - fastify.decorate('someSupport', function () { - return 'hugs' - }) -}) + fastify.decorate("someSupport", function () { + return "hugs"; + }); +}); diff --git a/plugins/swagger.ts b/plugins/swagger.ts index fb78fb66..d21f2ca2 100644 --- a/plugins/swagger.ts +++ b/plugins/swagger.ts @@ -1,29 +1,29 @@ -import fp from 'fastify-plugin' -import fastifySwaggerUi from '@fastify/swagger-ui' -import fastifySwagger from '@fastify/swagger' +import fp from "fastify-plugin"; +import fastifySwaggerUi from "@fastify/swagger-ui"; +import fastifySwagger from "@fastify/swagger"; export default fp(async function (fastify) { /** - * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas - * - * @see https://github.com/fastify/fastify-swagger - */ + * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas + * + * @see https://github.com/fastify/fastify-swagger + */ await fastify.register(fastifySwagger, { openapi: { info: { - title: 'Fastify demo API', - description: 'The official Fastify demo API', - version: '0.0.0' - } - } - }) + title: "Fastify demo API", + description: "The official Fastify demo API", + version: "0.0.0", + }, + }, + }); /** - * A Fastify plugin for serving Swagger UI. - * - * @see https://github.com/fastify/fastify-swagger-ui - */ + * A Fastify plugin for serving Swagger UI. + * + * @see https://github.com/fastify/fastify-swagger-ui + */ await fastify.register(fastifySwaggerUi, { // Set plugin options here - }) -}) + }); +}); diff --git a/routes/example/index.ts b/routes/example/index.ts index c75e37ec..30864859 100644 --- a/routes/example/index.ts +++ b/routes/example/index.ts @@ -1,19 +1,26 @@ -import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' +import { + FastifyPluginAsyncTypebox, + Type, +} from "@fastify/type-provider-typebox"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { // Prefixed with /example by the autoloader - fastify.get('/', { - schema: { - response: { - 200: Type.Object({ - message: Type.String() - }) + fastify.get( + "/", + { + schema: { + response: { + 200: Type.Object({ + message: Type.String(), + }), + }, + tags: ["Example"], }, - tags: ['Example'] - } - }, async function (request, reply) { - return { message: 'This is an example' } - }) -} + }, + async function () { + return { message: "This is an example" }; + }, + ); +}; -export default plugin +export default plugin; diff --git a/routes/home.ts b/routes/home.ts index 72c92146..91fc34a2 100644 --- a/routes/home.ts +++ b/routes/home.ts @@ -1,18 +1,25 @@ -import { FastifyPluginAsyncTypebox, Type } from '@fastify/type-provider-typebox' +import { + FastifyPluginAsyncTypebox, + Type, +} from "@fastify/type-provider-typebox"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { - fastify.get('/', { - schema: { - response: { - 200: Type.Object({ - message: Type.String() - }) + fastify.get( + "/", + { + schema: { + response: { + 200: Type.Object({ + message: Type.String(), + }), + }, + tags: ["Home"], }, - tags: ['Home'] - } - }, async function (request, reply) { - return { message: 'Welcome to the official fastify demo!' } - }) -} + }, + async function () { + return { message: "Welcome to the official fastify demo!" }; + }, + ); +}; -export default plugin +export default plugin; diff --git a/server.ts b/server.ts index 584d171e..f64cc8d6 100644 --- a/server.ts +++ b/server.ts @@ -5,69 +5,72 @@ * You can launch it with the command `npm run standalone` */ -import Fastify from 'fastify' +import Fastify from "fastify"; // Import library to exit fastify process, gracefully (if possible) -import closeWithGrace from 'close-with-grace' +import closeWithGrace from "close-with-grace"; // Import your application as a normal plugin. -import serviceApp from './app.js' +import serviceApp from "./app.js"; -const environment = process.env.NODE_ENV ?? 'production' +const environment = process.env.NODE_ENV ?? "production"; const envToLogger = { development: { - level: 'info', + level: "info", transport: { - target: 'pino-pretty', + target: "pino-pretty", options: { - translateTime: 'HH:MM:ss Z', - ignore: 'pid,hostname' - } - } + translateTime: "HH:MM:ss Z", + ignore: "pid,hostname", + }, + }, }, production: true, - test: false -} + test: false, +}; const app = Fastify({ logger: envToLogger[environment] ?? true, ajv: { customOptions: { - coerceTypes: 'array', // change data type of data to match type keyword - removeAdditional: 'all',// Remove additional body properties + coerceTypes: "array", // change data type of data to match type keyword + removeAdditional: "all", // Remove additional body properties }, }, -}) +}); -async function init () { +async function init() { // Register your application as a normal plugin. - app.register(serviceApp) + app.register(serviceApp); // console.log(app.config("hello")) // Delay is the number of milliseconds for the graceful close to finish - const closeListeners = closeWithGrace({ delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 500 }, async ({ err }) => { - if (err != null) { - app.log.error(err) - } + const closeListeners = closeWithGrace( + { delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 500 }, + async ({ err }) => { + if (err != null) { + app.log.error(err); + } - await app.close() - }) + await app.close(); + }, + ); - app.addHook('onClose', (instance, done) => { - closeListeners.uninstall() - done() - }) + app.addHook("onClose", (instance, done) => { + closeListeners.uninstall(); + done(); + }); - await app.ready() + await app.ready(); // Start listening. app.listen({ port: process.env.PORT ?? 3000 }, (err) => { if (err != null) { - app.log.error(err) - process.exit(1) + app.log.error(err); + process.exit(1); } - }) + }); } -init() +init(); diff --git a/test/helper.ts b/test/helper.ts index 140b5932..3b99646f 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -1,34 +1,34 @@ // This file contains code that we reuse // between our tests. -import { build as buildApplication } from 'fastify-cli/helper.js' -import path from 'node:path' -import { TestContext } from 'node:test' -import { fileURLToPath } from 'node:url' +import { build as buildApplication } from "fastify-cli/helper.js"; +import path from "node:path"; +import { TestContext } from "node:test"; +import { fileURLToPath } from "node:url"; -const __filename = fileURLToPath(import.meta.url) -const __dirname = path.dirname(__filename) +const __filename = fileURLToPath(import.meta.url); +const __dirname = path.dirname(__filename); -const AppPath = path.join(__dirname, '../app.js') +const AppPath = path.join(__dirname, "../app.js"); // Fill in this config with all the configurations // needed for testing the application -export function config () { - return {} +export function config() { + return {}; } // automatically build and tear down our instance -export async function build (t: TestContext) { +export async function build(t: TestContext) { // you can set all the options supported by the fastify CLI command - const argv = [AppPath] + const argv = [AppPath]; // fastify-plugin ensures that all decorators // are exposed for testing purposes, this is // different from the production setup - const app = await buildApplication(argv, config()) + const app = await buildApplication(argv, config()); // close the app after we are done - t.after(() => app.close()) + t.after(() => app.close()); - return app + return app; } diff --git a/test/plugins/support.test.ts b/test/plugins/support.test.ts index 44da2667..306543e6 100644 --- a/test/plugins/support.test.ts +++ b/test/plugins/support.test.ts @@ -1,16 +1,16 @@ -import { test } from 'node:test' -import assert from 'node:assert' +import { test } from "node:test"; +import assert from "node:assert"; -import Fastify from 'fastify' -import Support from '../../plugins/support.js' +import Fastify from "fastify"; +import Support from "../../plugins/support.js"; -test('support works standalone', async () => { - const fastify = Fastify() - fastify.register(Support) +test("support works standalone", async () => { + const fastify = Fastify(); + fastify.register(Support); - await fastify.ready() - assert.equal(fastify.someSupport(), 'hugs') -}) + await fastify.ready(); + assert.equal(fastify.someSupport(), "hugs"); +}); // You can also use plugin with opts in fastify v2 // diff --git a/test/routes/example.test.ts b/test/routes/example.test.ts index f8b83844..5a99eeef 100644 --- a/test/routes/example.test.ts +++ b/test/routes/example.test.ts @@ -1,13 +1,15 @@ -import { test } from 'node:test' -import assert from 'node:assert' -import { build } from '../helper.js' +import { test } from "node:test"; +import assert from "node:assert"; +import { build } from "../helper.js"; -test('example is loaded', async (t) => { - const app = await build(t) +test("example is loaded", async (t) => { + const app = await build(t); const res = await app.inject({ - url: '/example' - }) + url: "/example", + }); - assert.deepStrictEqual(JSON.parse(res.payload), { message: 'This is an example' }) -}) + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "This is an example", + }); +}); diff --git a/test/routes/home.test.ts b/test/routes/home.test.ts index 36b34a0c..3471561f 100644 --- a/test/routes/home.test.ts +++ b/test/routes/home.test.ts @@ -1,13 +1,15 @@ -import { test } from 'node:test' -import assert from 'node:assert' -import { build } from '../helper.js' +import { test } from "node:test"; +import assert from "node:assert"; +import { build } from "../helper.js"; -test('default root route', async (t) => { - const app = await build(t) +test("default root route", async (t) => { + const app = await build(t); const res = await app.inject({ - url: '/' - }) + url: "/", + }); - assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Welcome to the official fastify demo!' }) -}) + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "Welcome to the official fastify demo!", + }); +}); diff --git a/test/routes/not-found-handler.test.ts b/test/routes/not-found-handler.test.ts index e073fa0b..01050bc7 100644 --- a/test/routes/not-found-handler.test.ts +++ b/test/routes/not-found-handler.test.ts @@ -1,15 +1,15 @@ -import { test } from 'node:test' -import assert from 'node:assert' -import { build } from '../helper.js' +import { test } from "node:test"; +import assert from "node:assert"; +import { build } from "../helper.js"; -test('root not found handler', async (t) => { - const app = await build(t) +test("root not found handler", async (t) => { + const app = await build(t); const res = await app.inject({ - method: 'GET', - url: '/this-route-does-not-exist' - }) + method: "GET", + url: "/this-route-does-not-exist", + }); - assert.strictEqual(res.statusCode, 404) - assert.deepStrictEqual(JSON.parse(res.payload), { message: 'Not Found' }) -}) + assert.strictEqual(res.statusCode, 404); + assert.deepStrictEqual(JSON.parse(res.payload), { message: "Not Found" }); +}); diff --git a/tsconfig.json b/tsconfig.json index 868a7c0a..4dea4fee 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,6 @@ { - "extends": "fastify-tsconfig", - "compilerOptions": { - "outDir": "dist", - } -} \ No newline at end of file + "extends": "fastify-tsconfig", + "compilerOptions": { + "outDir": "dist" + } +} From 45cf1cbcb660fa5cfddbb7f391d37768741aeabb Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 5 Jun 2024 18:40:38 +0200 Subject: [PATCH 032/100] fix: add blank line to .prettierignore --- .prettierignore | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.prettierignore b/.prettierignore index 53c37a16..1521c8b7 100644 --- a/.prettierignore +++ b/.prettierignore @@ -1 +1 @@ -dist \ No newline at end of file +dist From f0f8d5c50897bd8b5d81e25cee65aed5d2873b2a Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 08:20:15 +0200 Subject: [PATCH 033/100] chore: add under-pressure plugin --- plugins/under-pressure.ts | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 plugins/under-pressure.ts diff --git a/plugins/under-pressure.ts b/plugins/under-pressure.ts new file mode 100644 index 00000000..080e5cbd --- /dev/null +++ b/plugins/under-pressure.ts @@ -0,0 +1,33 @@ +import { FastifyInstance } from "fastify"; +import fastifyUnderPressure, { + UnderPressureOptions, +} from "@fastify/under-pressure"; +import fp from "fastify-plugin"; + +const opts = (/* parent: FastifyInstance */) => { + return { + maxEventLoopDelay: 1000, + maxHeapUsedBytes: 100000000, + maxRssBytes: 100000000, + maxEventLoopUtilization: 0.98, + message: "The server is under pressure, retry later!", + retryAfter: 50, + // @TODO + // healthCheck: async function () { + // const connection = await parent.mysql.getConnection(); + // try { + // await connection.query("SELECT 1"); + // return true; + // } catch (err) { + // throw new Error("Database connection is not available"); + // } finally { + // connection.release(); + // } + // }, + // healthCheckInterval: 5000, + } satisfies UnderPressureOptions; +}; + +export default fp(async function (fastify: FastifyInstance) { + fastify.register(fastifyUnderPressure, opts); +}); From 727d5e27e055d99e36b95ccdd6b1559cd65fefa4 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 08:25:45 +0200 Subject: [PATCH 034/100] docs: add links to under-pressure plugin --- plugins/under-pressure.ts | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/plugins/under-pressure.ts b/plugins/under-pressure.ts index 080e5cbd..e3b30c6d 100644 --- a/plugins/under-pressure.ts +++ b/plugins/under-pressure.ts @@ -28,6 +28,15 @@ const opts = (/* parent: FastifyInstance */) => { } satisfies UnderPressureOptions; }; +/** + * A Fastify plugin for mesuring process load and automatically + * handle of "Service Unavailable" + * + * @see https://github.com/fastify/under-pressure + * + * Video on the topic: Do not thrash the event loop + * @see https://www.youtube.com/watch?v=VI29mUA8n9w + */ export default fp(async function (fastify: FastifyInstance) { fastify.register(fastifyUnderPressure, opts); }); From 57dce97ed0505d9a2273feb056d42b70a5e77f16 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 08:30:46 +0200 Subject: [PATCH 035/100] refactor: custom plugins should have a name --- plugins/support.ts | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/plugins/support.ts b/plugins/support.ts index 55d8fcd0..0708e0cd 100644 --- a/plugins/support.ts +++ b/plugins/support.ts @@ -10,4 +10,7 @@ export default fp(async function (fastify) { fastify.decorate("someSupport", function () { return "hugs"; }); -}); + + // You should name your custom plugins to avoid name collisions + // and to perform dependency checks. +}, { name: "support" }); From 2c5595c1ab0bf1e2a9bfffac0a2244e3fcdbf8d8 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 10:13:38 +0200 Subject: [PATCH 036/100] chore: setup db --- @types/fastify/index.d.ts | 11 +++++++++++ docker-compose.yml | 24 +++++++++++++++++++++++ package.json | 2 ++ plugins/env.ts | 25 +++++++++++++++++++++--- plugins/mysql.ts | 23 ++++++++++++++++++++++ plugins/support.ts | 18 ++++++++++-------- plugins/under-pressure.ts | 40 ++++++++++++++++++++++----------------- 7 files changed, 115 insertions(+), 28 deletions(-) create mode 100644 docker-compose.yml create mode 100644 plugins/mysql.ts diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts index 9f5ec4a2..029f5885 100644 --- a/@types/fastify/index.d.ts +++ b/@types/fastify/index.d.ts @@ -1,6 +1,17 @@ +import { MySQLPromisePool } from "@fastify/mysql"; + declare module "fastify" { export interface FastifyInstance { someSupport(): void; + mysql: MySQLPromisePool; + config: { + PORT: string; + MYSQL_HOST: string; + MYSQL_PORT: string; + MYSQL_USER: string; + MYSQL_PASSWORD: string; + MYSQL_DATABASE: string; + }; } } diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 00000000..ad939b11 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,24 @@ +services: + db: + image: mysql:8.4 + environment: + MYSQL_DATABASE: ${MYSQL_DATABASE} + MYSQL_USER: ${MYSQL_USER} + MYSQL_PASSWORD: ${MYSQL_PASSWORD} + ports: + - 3306:3306 + volumes: + - db_data:/var/lib/mysql + + phpmyadmin: + image: phpmyadmin/phpmyadmin + depends_on: + - db + ports: + - 8001:80 + environment: + PMA_HOST: db + PMA_PORT: ${MYSQL_PORT} + +volumes: + db_data: diff --git a/package.json b/package.json index 2b04fcf5..e907d5f8 100644 --- a/package.json +++ b/package.json @@ -25,10 +25,12 @@ "@fastify/cors": "^9.0.1", "@fastify/env": "^4.3.0", "@fastify/helmet": "^11.1.1", + "@fastify/mysql": "^4.3.0", "@fastify/sensible": "^5.0.0", "@fastify/swagger": "^8.14.0", "@fastify/swagger-ui": "^3.0.0", "@fastify/type-provider-typebox": "^4.0.0", + "@fastify/under-pressure": "^8.3.0", "@sinclair/typebox": "^0.32.31", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", diff --git a/plugins/env.ts b/plugins/env.ts index 7e204385..cca57451 100644 --- a/plugins/env.ts +++ b/plugins/env.ts @@ -3,11 +3,30 @@ import env from "@fastify/env"; const schema = { type: "object", - required: ["PORT"], + required: [ + "MYSQL_HOST", + "MYSQL_PORT", + "MYSQL_USER", + "MYSQL_PASSWORD", + "MYSQL_DATABASE", + ], properties: { - PORT: { + MYSQL_HOST: { + type: "string", + default: "localhost", + }, + MYSQL_PORT: { + type: "string", + default: "3306", + }, + MYSQL_USER: { + type: "string", + }, + MYSQL_PASSWORD: { + type: "string", + }, + MYSQL_DATABASE: { type: "string", - default: 3000, }, }, }; diff --git a/plugins/mysql.ts b/plugins/mysql.ts new file mode 100644 index 00000000..5c6a4938 --- /dev/null +++ b/plugins/mysql.ts @@ -0,0 +1,23 @@ +import fp from "fastify-plugin"; +import fastifyMysql from "@fastify/mysql"; + +/** + * This plugins allows to use `mysql2`. + * + * @see https://github.com/fastify/fastify-mysql + */ +export default fp( + async function (fastify) { + fastify.register(fastifyMysql, { + promise: true, + host: fastify.config.MYSQL_HOST, + user: fastify.config.MYSQL_USER, + password: fastify.config.MYSQL_PASSWORD, + database: fastify.config.MYSQL_DATABASE, + port: Number(fastify.config.MYSQL_PORT), + }); + }, + // You should name your plugins if you want to avoid name collisions + // and/or to perform dependency checks. + { name: "db" }, +); diff --git a/plugins/support.ts b/plugins/support.ts index 0708e0cd..2d67bc67 100644 --- a/plugins/support.ts +++ b/plugins/support.ts @@ -6,11 +6,13 @@ import fp from "fastify-plugin"; * * @see https://github.com/fastify/fastify-plugin */ -export default fp(async function (fastify) { - fastify.decorate("someSupport", function () { - return "hugs"; - }); - - // You should name your custom plugins to avoid name collisions - // and to perform dependency checks. -}, { name: "support" }); +export default fp( + async function (fastify) { + fastify.decorate("someSupport", function () { + return "hugs"; + }); + // You should name your plugins if you want to avoid name collisions + // and/or to perform dependency checks. + }, + { name: "support" }, +); diff --git a/plugins/under-pressure.ts b/plugins/under-pressure.ts index e3b30c6d..56c37c64 100644 --- a/plugins/under-pressure.ts +++ b/plugins/under-pressure.ts @@ -4,7 +4,7 @@ import fastifyUnderPressure, { } from "@fastify/under-pressure"; import fp from "fastify-plugin"; -const opts = (/* parent: FastifyInstance */) => { +const opts = (parent: FastifyInstance) => { return { maxEventLoopDelay: 1000, maxHeapUsedBytes: 100000000, @@ -12,19 +12,20 @@ const opts = (/* parent: FastifyInstance */) => { maxEventLoopUtilization: 0.98, message: "The server is under pressure, retry later!", retryAfter: 50, - // @TODO - // healthCheck: async function () { - // const connection = await parent.mysql.getConnection(); - // try { - // await connection.query("SELECT 1"); - // return true; - // } catch (err) { - // throw new Error("Database connection is not available"); - // } finally { - // connection.release(); - // } - // }, - // healthCheckInterval: 5000, + healthCheck: async () => { + let connection; + try { + connection = await parent.mysql.getConnection(); + await connection.query("SELECT 1;"); + return true; + } catch (err) { + parent.log.error(err, "healthCheck has failed"); + throw new Error("Database connection is not available"); + } finally { + connection?.release(); + } + }, + healthCheckInterval: 5000, } satisfies UnderPressureOptions; }; @@ -37,6 +38,11 @@ const opts = (/* parent: FastifyInstance */) => { * Video on the topic: Do not thrash the event loop * @see https://www.youtube.com/watch?v=VI29mUA8n9w */ -export default fp(async function (fastify: FastifyInstance) { - fastify.register(fastifyUnderPressure, opts); -}); +export default fp( + async function (fastify: FastifyInstance) { + fastify.register(fastifyUnderPressure, opts); + }, + { + dependencies: ["db"], + }, +); From 607910f230012c2f61476a361e621bb278921cc7 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 10:46:26 +0200 Subject: [PATCH 037/100] chore: remove phpmyadmin from docker composition --- docker-compose.yml | 10 ---------- 1 file changed, 10 deletions(-) diff --git a/docker-compose.yml b/docker-compose.yml index ad939b11..10830fc3 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -10,15 +10,5 @@ services: volumes: - db_data:/var/lib/mysql - phpmyadmin: - image: phpmyadmin/phpmyadmin - depends_on: - - db - ports: - - 8001:80 - environment: - PMA_HOST: db - PMA_PORT: ${MYSQL_PORT} - volumes: db_data: From 67f12f773bb45c6e80d28dcea2575292468fd4b3 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 11:43:55 +0200 Subject: [PATCH 038/100] fix: typo --- plugins/mysql.ts | 2 +- plugins/under-pressure.ts | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/plugins/mysql.ts b/plugins/mysql.ts index 5c6a4938..28707299 100644 --- a/plugins/mysql.ts +++ b/plugins/mysql.ts @@ -2,7 +2,7 @@ import fp from "fastify-plugin"; import fastifyMysql from "@fastify/mysql"; /** - * This plugins allows to use `mysql2`. + * This plugin allows using `mysql2` with Fastify. * * @see https://github.com/fastify/fastify-mysql */ diff --git a/plugins/under-pressure.ts b/plugins/under-pressure.ts index 56c37c64..5446756a 100644 --- a/plugins/under-pressure.ts +++ b/plugins/under-pressure.ts @@ -7,8 +7,8 @@ import fp from "fastify-plugin"; const opts = (parent: FastifyInstance) => { return { maxEventLoopDelay: 1000, - maxHeapUsedBytes: 100000000, - maxRssBytes: 100000000, + maxHeapUsedBytes: 100_000_000, + maxRssBytes: 100_000_000, maxEventLoopUtilization: 0.98, message: "The server is under pressure, retry later!", retryAfter: 50, From 1f4d3c85d695ad796a7acea86e2dd0ce6c77b78c Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 13:02:50 +0200 Subject: [PATCH 039/100] refactor: create src folder and run test folder with tap --- .gitignore | 3 +++ package.json | 3 ++- app.ts => src/app.ts | 0 {plugins => src/plugins}/README.md | 0 {plugins => src/plugins}/cors.ts | 0 {plugins => src/plugins}/env.ts | 0 {plugins => src/plugins}/helmet.ts | 0 {plugins => src/plugins}/mysql.ts | 0 {plugins => src/plugins}/sensible.ts | 0 {plugins => src/plugins}/support.ts | 0 {plugins => src/plugins}/swagger.ts | 0 {plugins => src/plugins}/under-pressure.ts | 0 {routes => src/routes}/README.md | 0 {routes => src/routes}/example/index.ts | 0 {routes => src/routes}/home.ts | 0 server.ts => src/server.ts | 0 test/helper.ts | 2 +- test/plugins/support.test.ts | 4 ++-- test/routes/error-handler.test.ts | 18 +++++++++++++++++- test/routes/example.test.ts | 2 ++ tsconfig.json | 6 ++++-- 21 files changed, 31 insertions(+), 7 deletions(-) rename app.ts => src/app.ts (100%) rename {plugins => src/plugins}/README.md (100%) rename {plugins => src/plugins}/cors.ts (100%) rename {plugins => src/plugins}/env.ts (100%) rename {plugins => src/plugins}/helmet.ts (100%) rename {plugins => src/plugins}/mysql.ts (100%) rename {plugins => src/plugins}/sensible.ts (100%) rename {plugins => src/plugins}/support.ts (100%) rename {plugins => src/plugins}/swagger.ts (100%) rename {plugins => src/plugins}/under-pressure.ts (100%) rename {routes => src/routes}/README.md (100%) rename {routes => src/routes}/example/index.ts (100%) rename {routes => src/routes}/home.ts (100%) rename server.ts => src/server.ts (100%) diff --git a/.gitignore b/.gitignore index 48ea4aeb..e4d563a7 100644 --- a/.gitignore +++ b/.gitignore @@ -26,6 +26,9 @@ coverage # nyc test coverage .nyc_output +# tap test coverage +.tap + # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) .grunt diff --git a/package.json b/package.json index e907d5f8..6aecff75 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "rm -rf dist && tsc", "watch": "npm run build -- --watch", - "test": "npm run build && glob -c \"node --test\" \"./test/**/*.dist.js\"", + "test": "tap test/**/*.ts", "start": "npm run build && fastify start -l info dist/app.js", "dev": "npm run build && fastify start -w -l info -P dist/app.js", "standalone": "npm run build && node --env-file=.env dist/server.js", @@ -44,6 +44,7 @@ "eslint": "^8.57.0", "fastify-tsconfig": "^2.0.0", "glob": "^10.4.1", + "tap": "^19.2.2", "ts-standard": "^12.0.2", "typescript": "^5.4.5", "typescript-eslint": "^7.12.0" diff --git a/app.ts b/src/app.ts similarity index 100% rename from app.ts rename to src/app.ts diff --git a/plugins/README.md b/src/plugins/README.md similarity index 100% rename from plugins/README.md rename to src/plugins/README.md diff --git a/plugins/cors.ts b/src/plugins/cors.ts similarity index 100% rename from plugins/cors.ts rename to src/plugins/cors.ts diff --git a/plugins/env.ts b/src/plugins/env.ts similarity index 100% rename from plugins/env.ts rename to src/plugins/env.ts diff --git a/plugins/helmet.ts b/src/plugins/helmet.ts similarity index 100% rename from plugins/helmet.ts rename to src/plugins/helmet.ts diff --git a/plugins/mysql.ts b/src/plugins/mysql.ts similarity index 100% rename from plugins/mysql.ts rename to src/plugins/mysql.ts diff --git a/plugins/sensible.ts b/src/plugins/sensible.ts similarity index 100% rename from plugins/sensible.ts rename to src/plugins/sensible.ts diff --git a/plugins/support.ts b/src/plugins/support.ts similarity index 100% rename from plugins/support.ts rename to src/plugins/support.ts diff --git a/plugins/swagger.ts b/src/plugins/swagger.ts similarity index 100% rename from plugins/swagger.ts rename to src/plugins/swagger.ts diff --git a/plugins/under-pressure.ts b/src/plugins/under-pressure.ts similarity index 100% rename from plugins/under-pressure.ts rename to src/plugins/under-pressure.ts diff --git a/routes/README.md b/src/routes/README.md similarity index 100% rename from routes/README.md rename to src/routes/README.md diff --git a/routes/example/index.ts b/src/routes/example/index.ts similarity index 100% rename from routes/example/index.ts rename to src/routes/example/index.ts diff --git a/routes/home.ts b/src/routes/home.ts similarity index 100% rename from routes/home.ts rename to src/routes/home.ts diff --git a/server.ts b/src/server.ts similarity index 100% rename from server.ts rename to src/server.ts diff --git a/test/helper.ts b/test/helper.ts index 3b99646f..559e76a4 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -9,7 +9,7 @@ import { fileURLToPath } from "node:url"; const __filename = fileURLToPath(import.meta.url); const __dirname = path.dirname(__filename); -const AppPath = path.join(__dirname, "../app.js"); +const AppPath = path.join(__dirname, "../src/app.ts"); // Fill in this config with all the configurations // needed for testing the application diff --git a/test/plugins/support.test.ts b/test/plugins/support.test.ts index 306543e6..385fcc6b 100644 --- a/test/plugins/support.test.ts +++ b/test/plugins/support.test.ts @@ -1,8 +1,8 @@ -import { test } from "node:test"; +import { test } from "tap"; import assert from "node:assert"; import Fastify from "fastify"; -import Support from "../../plugins/support.js"; +import Support from "../../src/plugins/support.js"; test("support works standalone", async () => { const fastify = Fastify(); diff --git a/test/routes/error-handler.test.ts b/test/routes/error-handler.test.ts index d012b00e..cb16255b 100644 --- a/test/routes/error-handler.test.ts +++ b/test/routes/error-handler.test.ts @@ -1 +1,17 @@ -// TODO: See fastify-cli to add route throwing and error before ready is called +import { test } from "node:test"; +import assert from "node:assert"; +import fastify from "fastify"; +import serviceApp from "../../src/app.ts" + +test("root not found handler", async (t) => { + const app = fastify() ; + app.register(serviceApp) + + const res = await app.inject({ + method: "GET", + url: "/this-route-does-not-exist", + }); + + assert.strictEqual(res.statusCode, 404); + assert.deepStrictEqual(JSON.parse(res.payload), { message: "Not Found" }); +}); diff --git a/test/routes/example.test.ts b/test/routes/example.test.ts index 5a99eeef..232f0136 100644 --- a/test/routes/example.test.ts +++ b/test/routes/example.test.ts @@ -12,4 +12,6 @@ test("example is loaded", async (t) => { assert.deepStrictEqual(JSON.parse(res.payload), { message: "This is an example", }); + + app.listen() }); diff --git a/tsconfig.json b/tsconfig.json index 4dea4fee..c0419d5d 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -1,6 +1,8 @@ { "extends": "fastify-tsconfig", "compilerOptions": { - "outDir": "dist" - } + "outDir": "dist", + "typeRoots": ["@types", "node_modules/@types"] + }, + "include": ["@types", "src/**/*.ts"] } From 33f5fccd30fc77b5461289a36f1e29679b6b4629 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 13:04:06 +0200 Subject: [PATCH 040/100] chore: remove glob --- package.json | 1 - 1 file changed, 1 deletion(-) diff --git a/package.json b/package.json index 6aecff75..3d777fae 100644 --- a/package.json +++ b/package.json @@ -43,7 +43,6 @@ "@types/node": "^20.14.2", "eslint": "^8.57.0", "fastify-tsconfig": "^2.0.0", - "glob": "^10.4.1", "tap": "^19.2.2", "ts-standard": "^12.0.2", "typescript": "^5.4.5", From 30a7a57dc67ba23d275a65a31b00a18628ca8656 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 13:05:02 +0200 Subject: [PATCH 041/100] test: error-handler still todo --- test/routes/error-handler.test.ts | 18 +----------------- 1 file changed, 1 insertion(+), 17 deletions(-) diff --git a/test/routes/error-handler.test.ts b/test/routes/error-handler.test.ts index cb16255b..d012b00e 100644 --- a/test/routes/error-handler.test.ts +++ b/test/routes/error-handler.test.ts @@ -1,17 +1 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import fastify from "fastify"; -import serviceApp from "../../src/app.ts" - -test("root not found handler", async (t) => { - const app = fastify() ; - app.register(serviceApp) - - const res = await app.inject({ - method: "GET", - url: "/this-route-does-not-exist", - }); - - assert.strictEqual(res.statusCode, 404); - assert.deepStrictEqual(JSON.parse(res.payload), { message: "Not Found" }); -}); +// TODO: See fastify-cli to add route throwing and error before ready is called From bc83e712d3ffb7e29c8992cded9f306b6326285f Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 15:11:13 +0200 Subject: [PATCH 042/100] fix: remove @types from tsconfig include --- @types/node/environment.d.ts | 4 ++-- test/routes/example.test.ts | 2 +- tsconfig.json | 2 +- 3 files changed, 4 insertions(+), 4 deletions(-) diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index 764f30ca..f68e7b6a 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -2,8 +2,8 @@ declare global { namespace NodeJS { interface ProcessEnv { NODE_ENV?: "development" | "production"; - PORT?: number; - FASTIFY_CLOSE_GRACE_DELAY?: number; + PORT?: string; + FASTIFY_CLOSE_GRACE_DELAY?: string; } } } diff --git a/test/routes/example.test.ts b/test/routes/example.test.ts index 232f0136..4f212198 100644 --- a/test/routes/example.test.ts +++ b/test/routes/example.test.ts @@ -13,5 +13,5 @@ test("example is loaded", async (t) => { message: "This is an example", }); - app.listen() + app.listen(); }); diff --git a/tsconfig.json b/tsconfig.json index c0419d5d..342de638 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -4,5 +4,5 @@ "outDir": "dist", "typeRoots": ["@types", "node_modules/@types"] }, - "include": ["@types", "src/**/*.ts"] + "include": ["src/**/*.ts"] } From e61a1b76005c2abbea6f803ae5cb70149f2cc2fa Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 15:29:58 +0200 Subject: [PATCH 043/100] fix: compiler config --- @types/fastify/index.d.ts | 2 +- src/plugins/env.ts | 6 +++--- tsconfig.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts index 029f5885..6761fb8c 100644 --- a/@types/fastify/index.d.ts +++ b/@types/fastify/index.d.ts @@ -5,7 +5,7 @@ declare module "fastify" { someSupport(): void; mysql: MySQLPromisePool; config: { - PORT: string; + PORT: number; MYSQL_HOST: string; MYSQL_PORT: string; MYSQL_USER: string; diff --git a/src/plugins/env.ts b/src/plugins/env.ts index cca57451..dc6fade7 100644 --- a/src/plugins/env.ts +++ b/src/plugins/env.ts @@ -16,8 +16,8 @@ const schema = { default: "localhost", }, MYSQL_PORT: { - type: "string", - default: "3306", + type: "number", + default: 3306, }, MYSQL_USER: { type: "string", @@ -43,7 +43,7 @@ const options = { dotenv: true, // or, pass config options available on dotenv module // dotenv: { - // path: `${__dirname}/.env`, + // path: `${import.meta.dirname}/.env`, // debug: true // } diff --git a/tsconfig.json b/tsconfig.json index 342de638..6fe21f5a 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -2,7 +2,7 @@ "extends": "fastify-tsconfig", "compilerOptions": { "outDir": "dist", - "typeRoots": ["@types", "node_modules/@types"] + "rootDir": "src" }, - "include": ["src/**/*.ts"] + "include": ["@types", "src/**/*.ts"] } From f0c8645fc4ceb4bf754fd5865e29badfbe46fafe Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 15:47:06 +0200 Subject: [PATCH 044/100] chore: get rid of NODE_ENV --- @types/node/environment.d.ts | 5 ++--- src/server.ts | 37 +++++++++++++++++++----------------- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index f68e7b6a..c33914da 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -1,9 +1,8 @@ declare global { namespace NodeJS { interface ProcessEnv { - NODE_ENV?: "development" | "production"; - PORT?: string; - FASTIFY_CLOSE_GRACE_DELAY?: string; + PORT: number; + FASTIFY_CLOSE_GRACE_DELAY: number; } } } diff --git a/src/server.ts b/src/server.ts index f64cc8d6..7017e047 100644 --- a/src/server.ts +++ b/src/server.ts @@ -13,24 +13,29 @@ import closeWithGrace from "close-with-grace"; // Import your application as a normal plugin. import serviceApp from "./app.js"; -const environment = process.env.NODE_ENV ?? "production"; -const envToLogger = { - development: { - level: "info", - transport: { - target: "pino-pretty", - options: { - translateTime: "HH:MM:ss Z", - ignore: "pid,hostname", +/** + * Do not use NODE_ENV to determine what logger (or any env related feature) to use + * @see https://www.youtube.com/watch?v=HMM7GJC5E2o + */ +function getLoggerOptions() { + if (process.env.LOGGING === "pretty") { + return { + level: "info", + transport: { + target: "pino-pretty", + options: { + translateTime: "HH:MM:ss Z", + ignore: "pid,hostname", + }, }, - }, - }, - production: true, - test: false, -}; + }; + } + + return process.env.LOGGING === "default"; +} const app = Fastify({ - logger: envToLogger[environment] ?? true, + logger: getLoggerOptions(), ajv: { customOptions: { coerceTypes: "array", // change data type of data to match type keyword @@ -43,8 +48,6 @@ async function init() { // Register your application as a normal plugin. app.register(serviceApp); - // console.log(app.config("hello")) - // Delay is the number of milliseconds for the graceful close to finish const closeListeners = closeWithGrace( { delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 500 }, From 540e60aa367c4743b79758df32440056a3455fa2 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 15:47:55 +0200 Subject: [PATCH 045/100] fix: remove commented code --- test/plugins/support.test.ts | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/test/plugins/support.test.ts b/test/plugins/support.test.ts index 385fcc6b..9addc9cf 100644 --- a/test/plugins/support.test.ts +++ b/test/plugins/support.test.ts @@ -11,16 +11,3 @@ test("support works standalone", async () => { await fastify.ready(); assert.equal(fastify.someSupport(), "hugs"); }); - -// You can also use plugin with opts in fastify v2 -// -// test('support works standalone', (t) => { -// t.plan(2) -// const fastify = Fastify() -// fastify.register(Support) -// -// fastify.ready((err) => { -// t.error(err) -// assert.equal(fastify.someSupport(), 'hugs') -// }) -// }) From 08076bfc0c6d79493ff6e34e4648369cc203b191 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 15:49:54 +0200 Subject: [PATCH 046/100] refactor: move under-pressure options configuration inside plugin definition --- src/plugins/under-pressure.ts | 52 ++++++++++++++++------------------- 1 file changed, 23 insertions(+), 29 deletions(-) diff --git a/src/plugins/under-pressure.ts b/src/plugins/under-pressure.ts index 5446756a..8c81cbc9 100644 --- a/src/plugins/under-pressure.ts +++ b/src/plugins/under-pressure.ts @@ -1,34 +1,7 @@ import { FastifyInstance } from "fastify"; -import fastifyUnderPressure, { - UnderPressureOptions, -} from "@fastify/under-pressure"; +import fastifyUnderPressure from "@fastify/under-pressure"; import fp from "fastify-plugin"; -const opts = (parent: FastifyInstance) => { - return { - maxEventLoopDelay: 1000, - maxHeapUsedBytes: 100_000_000, - maxRssBytes: 100_000_000, - maxEventLoopUtilization: 0.98, - message: "The server is under pressure, retry later!", - retryAfter: 50, - healthCheck: async () => { - let connection; - try { - connection = await parent.mysql.getConnection(); - await connection.query("SELECT 1;"); - return true; - } catch (err) { - parent.log.error(err, "healthCheck has failed"); - throw new Error("Database connection is not available"); - } finally { - connection?.release(); - } - }, - healthCheckInterval: 5000, - } satisfies UnderPressureOptions; -}; - /** * A Fastify plugin for mesuring process load and automatically * handle of "Service Unavailable" @@ -40,7 +13,28 @@ const opts = (parent: FastifyInstance) => { */ export default fp( async function (fastify: FastifyInstance) { - fastify.register(fastifyUnderPressure, opts); + fastify.register(fastifyUnderPressure, { + maxEventLoopDelay: 1000, + maxHeapUsedBytes: 100_000_000, + maxRssBytes: 100_000_000, + maxEventLoopUtilization: 0.98, + message: "The server is under pressure, retry later!", + retryAfter: 50, + healthCheck: async () => { + let connection; + try { + connection = await fastify.mysql.getConnection(); + await connection.query("SELECT 1;"); + return true; + } catch (err) { + fastify.log.error(err, "healthCheck has failed"); + throw new Error("Database connection is not available"); + } finally { + connection?.release(); + } + }, + healthCheckInterval: 5000, + }); }, { dependencies: ["db"], From afa52ec4b968467cc699cf97e005519b46d61a61 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 15:51:34 +0200 Subject: [PATCH 047/100] refactor: leverage import.meta.dirname --- src/app.ts | 8 ++------ test/helper.ts | 6 +----- 2 files changed, 3 insertions(+), 11 deletions(-) diff --git a/src/app.ts b/src/app.ts index 1f08012c..d8384814 100644 --- a/src/app.ts +++ b/src/app.ts @@ -3,13 +3,9 @@ */ import fastifyAutoload from "@fastify/autoload"; -import { fileURLToPath } from "node:url"; import path from "node:path"; import { FastifyInstance, FastifyPluginOptions } from "fastify"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - export default async function serviceApp( fastify: FastifyInstance, opts: FastifyPluginOptions, @@ -18,14 +14,14 @@ export default async function serviceApp( // those should be support plugins that are reused // through your application fastify.register(fastifyAutoload, { - dir: path.join(__dirname, "plugins"), + dir: path.join(import.meta.dirname, "plugins"), options: { ...opts }, }); // This loads all plugins defined in routes // define your routes in one of these fastify.register(fastifyAutoload, { - dir: path.join(__dirname, "routes"), + dir: path.join(import.meta.dirname, "routes"), options: { ...opts }, }); diff --git a/test/helper.ts b/test/helper.ts index 559e76a4..98a8e977 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -4,12 +4,8 @@ import { build as buildApplication } from "fastify-cli/helper.js"; import path from "node:path"; import { TestContext } from "node:test"; -import { fileURLToPath } from "node:url"; -const __filename = fileURLToPath(import.meta.url); -const __dirname = path.dirname(__filename); - -const AppPath = path.join(__dirname, "../src/app.ts"); +const AppPath = path.join(import.meta.dirname, "../src/app.ts"); // Fill in this config with all the configurations // needed for testing the application From e4dce3495b503026c253455a19791904250cced2 Mon Sep 17 00:00:00 2001 From: jean Date: Fri, 7 Jun 2024 16:55:02 +0200 Subject: [PATCH 048/100] test: POST /example and error-handler --- package.json | 2 +- src/routes/example/index.ts | 22 ++++++++++ test/routes/error-handler.test.ts | 28 ++++++++++++- test/routes/example.test.ts | 17 -------- test/routes/example/example.test.ts | 58 +++++++++++++++++++++++++++ test/routes/home.test.ts | 2 +- test/routes/not-found-handler.test.ts | 4 +- 7 files changed, 111 insertions(+), 22 deletions(-) delete mode 100644 test/routes/example.test.ts create mode 100644 test/routes/example/example.test.ts diff --git a/package.json b/package.json index 3d777fae..93778e43 100644 --- a/package.json +++ b/package.json @@ -10,7 +10,7 @@ "scripts": { "build": "rm -rf dist && tsc", "watch": "npm run build -- --watch", - "test": "tap test/**/*.ts", + "test": "tap test/**/*", "start": "npm run build && fastify start -l info dist/app.js", "dev": "npm run build && fastify start -w -l info -P dist/app.js", "standalone": "npm run build && node --env-file=.env dist/server.js", diff --git a/src/routes/example/index.ts b/src/routes/example/index.ts index 30864859..c054e9d3 100644 --- a/src/routes/example/index.ts +++ b/src/routes/example/index.ts @@ -21,6 +21,28 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { return { message: "This is an example" }; }, ); + + fastify.post( + "/", + { + schema: { + body: Type.Object({ + digit: Type.Number({ minimum: 0, maximum: 9 }), + }), + response: { + 200: Type.Object({ + message: Type.String(), + }), + }, + tags: ["Example"], + }, + }, + async function (req) { + const { digit } = req.body; + + return { message: `Here is the digit you sent: ${digit}` }; + }, + ); }; export default plugin; diff --git a/test/routes/error-handler.test.ts b/test/routes/error-handler.test.ts index d012b00e..2f19d96a 100644 --- a/test/routes/error-handler.test.ts +++ b/test/routes/error-handler.test.ts @@ -1 +1,27 @@ -// TODO: See fastify-cli to add route throwing and error before ready is called +import { it } from "node:test"; +import assert from "node:assert"; +import fastify from "fastify"; +import serviceApp from "../../src/app.ts"; +import fp from "fastify-plugin"; + +it("should call errorHandler", async (t) => { + const app = fastify(); + await app.register(fp(serviceApp)); + + app.get("/error", () => { + throw new Error("Kaboom!"); + }); + + await app.ready(); + + t.after(() => app.close()); + + const res = await app.inject({ + method: "GET", + url: "/error", + }); + + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "Internal Server Error", + }); +}); diff --git a/test/routes/example.test.ts b/test/routes/example.test.ts deleted file mode 100644 index 4f212198..00000000 --- a/test/routes/example.test.ts +++ /dev/null @@ -1,17 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import { build } from "../helper.js"; - -test("example is loaded", async (t) => { - const app = await build(t); - - const res = await app.inject({ - url: "/example", - }); - - assert.deepStrictEqual(JSON.parse(res.payload), { - message: "This is an example", - }); - - app.listen(); -}); diff --git a/test/routes/example/example.test.ts b/test/routes/example/example.test.ts new file mode 100644 index 00000000..e6ca5310 --- /dev/null +++ b/test/routes/example/example.test.ts @@ -0,0 +1,58 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { build } from "../../helper.js"; + +test("GET /example", async (t) => { + const app = await build(t); + + const res = await app.inject({ + url: "/example", + }); + + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "This is an example", + }); + + app.listen(); +}); + +test("POST /example with valid body", async (t) => { + const app = await build(t); + + const res = await app.inject({ + method: "POST", + url: "/example", + payload: { + digit: 5, + }, + }); + + assert.strictEqual(res.statusCode, 200); + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "Here is the digit you sent: 5", + }); +}); + +test("POST /example with invalid body", async (t) => { + const app = await build(t); + + const testCases = [ + { digit: 10, description: "too high" }, + { digit: -1, description: "too low" }, + { digit: "a", description: "not a number" }, + ]; + + for (const testCase of testCases) { + const res = await app.inject({ + method: "POST", + url: "/example", + payload: { digit: testCase.digit }, + }); + + assert.strictEqual( + res.statusCode, + 400, + `Failed for case: ${testCase.description}`, + ); + } +}); diff --git a/test/routes/home.test.ts b/test/routes/home.test.ts index 3471561f..5edd1e65 100644 --- a/test/routes/home.test.ts +++ b/test/routes/home.test.ts @@ -2,7 +2,7 @@ import { test } from "node:test"; import assert from "node:assert"; import { build } from "../helper.js"; -test("default root route", async (t) => { +test("GET /", async (t) => { const app = await build(t); const res = await app.inject({ diff --git a/test/routes/not-found-handler.test.ts b/test/routes/not-found-handler.test.ts index 01050bc7..66b6b7f8 100644 --- a/test/routes/not-found-handler.test.ts +++ b/test/routes/not-found-handler.test.ts @@ -1,8 +1,8 @@ -import { test } from "node:test"; +import { it } from "node:test"; import assert from "node:assert"; import { build } from "../helper.js"; -test("root not found handler", async (t) => { +it("should call notFoundHandler", async (t) => { const app = await build(t); const res = await app.inject({ From c7379abab449e9e84966ceef6f207107b0b74977 Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 9 Jun 2024 18:28:31 +0200 Subject: [PATCH 049/100] chore: pretty-logger only if the program is running in an interactive terminal --- @types/node/environment.d.ts | 1 + src/server.ts | 9 ++++++--- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index c33914da..42ce748d 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -2,6 +2,7 @@ declare global { namespace NodeJS { interface ProcessEnv { PORT: number; + LOGGING?: number; FASTIFY_CLOSE_GRACE_DELAY: number; } } diff --git a/src/server.ts b/src/server.ts index 7017e047..aee95fd1 100644 --- a/src/server.ts +++ b/src/server.ts @@ -18,7 +18,8 @@ import serviceApp from "./app.js"; * @see https://www.youtube.com/watch?v=HMM7GJC5E2o */ function getLoggerOptions() { - if (process.env.LOGGING === "pretty") { + // Only if the program is running in an interactive terminal + if (process.stdout.isTTY) { return { level: "info", transport: { @@ -31,14 +32,16 @@ function getLoggerOptions() { }; } - return process.env.LOGGING === "default"; + // Don't forget to configure it with + // a truthy value in production + return !!process.env.LOGGING; } const app = Fastify({ logger: getLoggerOptions(), ajv: { customOptions: { - coerceTypes: "array", // change data type of data to match type keyword + coerceTypes: "array", // change type of data to match type keyword removeAdditional: "all", // Remove additional body properties }, }, From 831d593a5e0d1e263e5c5f281ea4e5d09424b293 Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 9 Jun 2024 18:29:40 +0200 Subject: [PATCH 050/100] fix: fp must be used to override default error handler --- src/server.ts | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/server.ts b/src/server.ts index aee95fd1..e959da1e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -6,6 +6,7 @@ */ import Fastify from "fastify"; +import fp from "fastify-plugin"; // Import library to exit fastify process, gracefully (if possible) import closeWithGrace from "close-with-grace"; @@ -32,7 +33,7 @@ function getLoggerOptions() { }; } - // Don't forget to configure it with + // Don't forget to configure it with // a truthy value in production return !!process.env.LOGGING; } @@ -49,7 +50,8 @@ const app = Fastify({ async function init() { // Register your application as a normal plugin. - app.register(serviceApp); + // fp must be used to override default error handler + app.register(fp(serviceApp)); // Delay is the number of milliseconds for the graceful close to finish const closeListeners = closeWithGrace( From 243b2e0218c00cb9cf3cb95b84aa6dfe94dd7f79 Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 9 Jun 2024 18:49:26 +0200 Subject: [PATCH 051/100] chore: update CI --- .github/workflows/ci.yml | 59 ++++++++++++++++++++++++++++++++++++---- 1 file changed, 53 insertions(+), 6 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 41f29272..f0da01d4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -1,9 +1,9 @@ name: CI + on: push: branches: - main - - master - next - "v*" paths-ignore: @@ -13,9 +13,56 @@ on: paths-ignore: - "docs/**" - "*.md" + jobs: - test: - uses: fastify/workflows/.github/workflows/plugins-ci.yml@v4.1.0 - with: - license-check: true - lint: true + build: + runs-on: ${{ matrix.os }} + strategy: + matrix: + os: [ubuntu-latest, windows-latest, macos-latest] + node-version: [22] + + services: + mysql: + image: mysql:8.4 + ports: + - 3306:3306 + env: + MYSQL_ROOT_PASSWORD: root_password + MYSQL_DATABASE: test_db + MYSQL_USER: test_user + MYSQL_PASSWORD: test_password + options: >- + --health-cmd="mysqladmin ping -u$MYSQL_USER -p$MYSQL_PASSWORD" + --health-interval=10s + --health-timeout=5s + --health-retries=3 + + steps: + - uses: actions/checkout@v4 + - name: Use Node.js ${{ matrix.node-version }} + uses: actions/setup-node@v4 + with: + node-version: ${{ matrix.node-version }} + + - name: Install dependencies + run: npm i --ignore-scripts + + - name: Lint Code + run: npm run lint + + - name: Wait for MySQL to be ready + run: | + until mysqladmin ping -h localhost -P 3306 --silent; do + echo 'Waiting for MySQL...' + sleep 3 + done + + - name: Test + env: + MYSQL_HOST: localhost + MYSQL_PORT: 3306 + MYSQL_DATABASE: test_db + MYSQL_USER: test_user + MYSQL_PASSWORD: test_password + run: npm run test From 9e3fcd6387221bbd6fbc6a819d308d733e4fb73f Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 9 Jun 2024 19:01:54 +0200 Subject: [PATCH 052/100] support only ubuntu --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0da01d4..f794b16b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,9 @@ on: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] node-version: [22] services: From b02248682b9368893615a435143b93333a221e18 Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 9 Jun 2024 19:04:48 +0200 Subject: [PATCH 053/100] dont wait for mysql to be ready --- .github/workflows/ci.yml | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f794b16b..efefd182 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,14 +49,7 @@ jobs: - name: Lint Code run: npm run lint - - - name: Wait for MySQL to be ready - run: | - until mysqladmin ping -h localhost -P 3306 --silent; do - echo 'Waiting for MySQL...' - sleep 3 - done - + - name: Test env: MYSQL_HOST: localhost From 58e03807edc232f18e26ba1fc50496738c376a0e Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 9 Jun 2024 19:07:17 +0200 Subject: [PATCH 054/100] lint --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index efefd182..fe2cd42c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -49,7 +49,7 @@ jobs: - name: Lint Code run: npm run lint - + - name: Test env: MYSQL_HOST: localhost From d7c2cd36c823cb6e5127c98bc381fbf54e70f4b8 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 10 Jun 2024 15:54:49 +0200 Subject: [PATCH 055/100] refactor: uninstall global listeners is not needed --- src/server.ts | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/src/server.ts b/src/server.ts index e959da1e..9579963d 100644 --- a/src/server.ts +++ b/src/server.ts @@ -54,7 +54,7 @@ async function init() { app.register(fp(serviceApp)); // Delay is the number of milliseconds for the graceful close to finish - const closeListeners = closeWithGrace( + closeWithGrace( { delay: process.env.FASTIFY_CLOSE_GRACE_DELAY ?? 500 }, async ({ err }) => { if (err != null) { @@ -65,11 +65,6 @@ async function init() { }, ); - app.addHook("onClose", (instance, done) => { - closeListeners.uninstall(); - done(); - }); - await app.ready(); // Start listening. From fb902bc4afebb56c149e8770b35b19ac20c7113d Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 10 Jun 2024 16:11:51 +0200 Subject: [PATCH 056/100] refactor: use neostandard --- .prettierignore | 1 - eslint.config.js | 25 ++++++++++++++++++------- package.json | 16 ++++++---------- src/app.ts | 18 +++++++++--------- src/plugins/env.ts | 18 +++++++++--------- src/plugins/mysql.ts | 4 ++-- src/plugins/support.ts | 2 +- src/plugins/swagger.ts | 6 +++--- src/plugins/under-pressure.ts | 6 +++--- src/routes/example/index.ts | 24 ++++++++++++------------ src/routes/home.ts | 12 ++++++------ src/server.ts | 14 +++++++------- test/routes/error-handler.test.ts | 4 ++-- test/routes/example/example.test.ts | 16 ++++++++-------- test/routes/home.test.ts | 4 ++-- test/routes/not-found-handler.test.ts | 2 +- 16 files changed, 89 insertions(+), 83 deletions(-) delete mode 100644 .prettierignore diff --git a/.prettierignore b/.prettierignore deleted file mode 100644 index 1521c8b7..00000000 --- a/.prettierignore +++ /dev/null @@ -1 +0,0 @@ -dist diff --git a/eslint.config.js b/eslint.config.js index 1eb1b43f..b9f40bf5 100644 --- a/eslint.config.js +++ b/eslint.config.js @@ -1,9 +1,20 @@ -// @ts-check +'use strict' -import eslint from "@eslint/js"; -import tseslint from "typescript-eslint"; +import neo from 'neostandard' -export default tseslint.config( - eslint.configs.recommended, - ...tseslint.configs.recommended, -); +export default [ + ...neo({ + ts: true + }), + { + rules: { + '@stylistic/comma-dangle': ['error', { + arrays: 'never', + objects: 'never', + imports: 'never', + exports: 'never', + functions: 'never' + }] + } + } +] diff --git a/package.json b/package.json index 93778e43..96ed8cfc 100644 --- a/package.json +++ b/package.json @@ -14,8 +14,8 @@ "start": "npm run build && fastify start -l info dist/app.js", "dev": "npm run build && fastify start -w -l info -P dist/app.js", "standalone": "npm run build && node --env-file=.env dist/server.js", - "lint": "prettier . --list-different && eslint . --ignore-pattern=dist", - "prettier": "prettier . --write" + "lint": "eslint . --ignore-pattern=dist", + "lint:fix": "npm run lint -- --fix" }, "keywords": [], "author": "Michelet Jean ", @@ -34,18 +34,14 @@ "@sinclair/typebox": "^0.32.31", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", - "fastify-plugin": "^4.0.0", - "prettier": "^3.3.1" + "fastify-plugin": "^4.0.0" }, "devDependencies": { - "@eslint/js": "^9.4.0", - "@types/eslint__js": "^8.42.3", "@types/node": "^20.14.2", - "eslint": "^8.57.0", + "eslint": "^9.4.0", "fastify-tsconfig": "^2.0.0", + "neostandard": "^0.7.0", "tap": "^19.2.2", - "ts-standard": "^12.0.2", - "typescript": "^5.4.5", - "typescript-eslint": "^7.12.0" + "typescript": "^5.4.5" } } diff --git a/src/app.ts b/src/app.ts index d8384814..ce8f0bbe 100644 --- a/src/app.ts +++ b/src/app.ts @@ -8,21 +8,21 @@ import { FastifyInstance, FastifyPluginOptions } from "fastify"; export default async function serviceApp( fastify: FastifyInstance, - opts: FastifyPluginOptions, + opts: FastifyPluginOptions ) { // This loads all plugins defined in plugins // those should be support plugins that are reused // through your application fastify.register(fastifyAutoload, { dir: path.join(import.meta.dirname, "plugins"), - options: { ...opts }, + options: { ...opts } }); // This loads all plugins defined in routes // define your routes in one of these fastify.register(fastifyAutoload, { dir: path.join(import.meta.dirname, "routes"), - options: { ...opts }, + options: { ...opts } }); fastify.setErrorHandler((err, request, reply) => { @@ -33,10 +33,10 @@ export default async function serviceApp( method: request.method, url: request.url, query: request.query, - params: request.params, - }, + params: request.params + } }, - "Unhandled error occurred", + "Unhandled error occurred" ); reply.code(err.statusCode ?? 500); @@ -51,10 +51,10 @@ export default async function serviceApp( method: request.method, url: request.url, query: request.query, - params: request.params, - }, + params: request.params + } }, - "Resource not found", + "Resource not found" ); reply.code(404); diff --git a/src/plugins/env.ts b/src/plugins/env.ts index dc6fade7..1d3ee178 100644 --- a/src/plugins/env.ts +++ b/src/plugins/env.ts @@ -8,27 +8,27 @@ const schema = { "MYSQL_PORT", "MYSQL_USER", "MYSQL_PASSWORD", - "MYSQL_DATABASE", + "MYSQL_DATABASE" ], properties: { MYSQL_HOST: { type: "string", - default: "localhost", + default: "localhost" }, MYSQL_PORT: { type: "number", - default: 3306, + default: 3306 }, MYSQL_USER: { - type: "string", + type: "string" }, MYSQL_PASSWORD: { - type: "string", + type: "string" }, MYSQL_DATABASE: { - type: "string", - }, - }, + type: "string" + } + } }; const options = { @@ -49,7 +49,7 @@ const options = { // Source for the configuration data // Optional, default: process.env - data: process.env, + data: process.env }; /** diff --git a/src/plugins/mysql.ts b/src/plugins/mysql.ts index 28707299..9aad7655 100644 --- a/src/plugins/mysql.ts +++ b/src/plugins/mysql.ts @@ -14,10 +14,10 @@ export default fp( user: fastify.config.MYSQL_USER, password: fastify.config.MYSQL_PASSWORD, database: fastify.config.MYSQL_DATABASE, - port: Number(fastify.config.MYSQL_PORT), + port: Number(fastify.config.MYSQL_PORT) }); }, // You should name your plugins if you want to avoid name collisions // and/or to perform dependency checks. - { name: "db" }, + { name: "db" } ); diff --git a/src/plugins/support.ts b/src/plugins/support.ts index 2d67bc67..79d8a983 100644 --- a/src/plugins/support.ts +++ b/src/plugins/support.ts @@ -14,5 +14,5 @@ export default fp( // You should name your plugins if you want to avoid name collisions // and/or to perform dependency checks. }, - { name: "support" }, + { name: "support" } ); diff --git a/src/plugins/swagger.ts b/src/plugins/swagger.ts index d21f2ca2..36470a1d 100644 --- a/src/plugins/swagger.ts +++ b/src/plugins/swagger.ts @@ -13,9 +13,9 @@ export default fp(async function (fastify) { info: { title: "Fastify demo API", description: "The official Fastify demo API", - version: "0.0.0", - }, - }, + version: "0.0.0" + } + } }); /** diff --git a/src/plugins/under-pressure.ts b/src/plugins/under-pressure.ts index 8c81cbc9..02e59e39 100644 --- a/src/plugins/under-pressure.ts +++ b/src/plugins/under-pressure.ts @@ -33,10 +33,10 @@ export default fp( connection?.release(); } }, - healthCheckInterval: 5000, + healthCheckInterval: 5000 }); }, { - dependencies: ["db"], - }, + dependencies: ["db"] + } ); diff --git a/src/routes/example/index.ts b/src/routes/example/index.ts index c054e9d3..db5aace2 100644 --- a/src/routes/example/index.ts +++ b/src/routes/example/index.ts @@ -1,6 +1,6 @@ import { FastifyPluginAsyncTypebox, - Type, + Type } from "@fastify/type-provider-typebox"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { @@ -11,15 +11,15 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { schema: { response: { 200: Type.Object({ - message: Type.String(), - }), + message: Type.String() + }) }, - tags: ["Example"], - }, + tags: ["Example"] + } }, async function () { return { message: "This is an example" }; - }, + } ); fastify.post( @@ -27,21 +27,21 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { { schema: { body: Type.Object({ - digit: Type.Number({ minimum: 0, maximum: 9 }), + digit: Type.Number({ minimum: 0, maximum: 9 }) }), response: { 200: Type.Object({ - message: Type.String(), - }), + message: Type.String() + }) }, - tags: ["Example"], - }, + tags: ["Example"] + } }, async function (req) { const { digit } = req.body; return { message: `Here is the digit you sent: ${digit}` }; - }, + } ); }; diff --git a/src/routes/home.ts b/src/routes/home.ts index 91fc34a2..9ab69595 100644 --- a/src/routes/home.ts +++ b/src/routes/home.ts @@ -1,6 +1,6 @@ import { FastifyPluginAsyncTypebox, - Type, + Type } from "@fastify/type-provider-typebox"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { @@ -10,15 +10,15 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { schema: { response: { 200: Type.Object({ - message: Type.String(), - }), + message: Type.String() + }) }, - tags: ["Home"], - }, + tags: ["Home"] + } }, async function () { return { message: "Welcome to the official fastify demo!" }; - }, + } ); }; diff --git a/src/server.ts b/src/server.ts index 9579963d..eee1a64f 100644 --- a/src/server.ts +++ b/src/server.ts @@ -27,9 +27,9 @@ function getLoggerOptions() { target: "pino-pretty", options: { translateTime: "HH:MM:ss Z", - ignore: "pid,hostname", - }, - }, + ignore: "pid,hostname" + } + } }; } @@ -43,9 +43,9 @@ const app = Fastify({ ajv: { customOptions: { coerceTypes: "array", // change type of data to match type keyword - removeAdditional: "all", // Remove additional body properties - }, - }, + removeAdditional: "all" // Remove additional body properties + } + } }); async function init() { @@ -62,7 +62,7 @@ async function init() { } await app.close(); - }, + } ); await app.ready(); diff --git a/test/routes/error-handler.test.ts b/test/routes/error-handler.test.ts index 2f19d96a..deed749e 100644 --- a/test/routes/error-handler.test.ts +++ b/test/routes/error-handler.test.ts @@ -18,10 +18,10 @@ it("should call errorHandler", async (t) => { const res = await app.inject({ method: "GET", - url: "/error", + url: "/error" }); assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Internal Server Error", + message: "Internal Server Error" }); }); diff --git a/test/routes/example/example.test.ts b/test/routes/example/example.test.ts index e6ca5310..7c8546ef 100644 --- a/test/routes/example/example.test.ts +++ b/test/routes/example/example.test.ts @@ -6,11 +6,11 @@ test("GET /example", async (t) => { const app = await build(t); const res = await app.inject({ - url: "/example", + url: "/example" }); assert.deepStrictEqual(JSON.parse(res.payload), { - message: "This is an example", + message: "This is an example" }); app.listen(); @@ -23,13 +23,13 @@ test("POST /example with valid body", async (t) => { method: "POST", url: "/example", payload: { - digit: 5, - }, + digit: 5 + } }); assert.strictEqual(res.statusCode, 200); assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Here is the digit you sent: 5", + message: "Here is the digit you sent: 5" }); }); @@ -39,20 +39,20 @@ test("POST /example with invalid body", async (t) => { const testCases = [ { digit: 10, description: "too high" }, { digit: -1, description: "too low" }, - { digit: "a", description: "not a number" }, + { digit: "a", description: "not a number" } ]; for (const testCase of testCases) { const res = await app.inject({ method: "POST", url: "/example", - payload: { digit: testCase.digit }, + payload: { digit: testCase.digit } }); assert.strictEqual( res.statusCode, 400, - `Failed for case: ${testCase.description}`, + `Failed for case: ${testCase.description}` ); } }); diff --git a/test/routes/home.test.ts b/test/routes/home.test.ts index 5edd1e65..6132997c 100644 --- a/test/routes/home.test.ts +++ b/test/routes/home.test.ts @@ -6,10 +6,10 @@ test("GET /", async (t) => { const app = await build(t); const res = await app.inject({ - url: "/", + url: "/" }); assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Welcome to the official fastify demo!", + message: "Welcome to the official fastify demo!" }); }); diff --git a/test/routes/not-found-handler.test.ts b/test/routes/not-found-handler.test.ts index 66b6b7f8..3e5d3adc 100644 --- a/test/routes/not-found-handler.test.ts +++ b/test/routes/not-found-handler.test.ts @@ -7,7 +7,7 @@ it("should call notFoundHandler", async (t) => { const res = await app.inject({ method: "GET", - url: "/this-route-does-not-exist", + url: "/this-route-does-not-exist" }); assert.strictEqual(res.statusCode, 404); From a39812c17c5fb93796e8a39c36810d9756940bd6 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 09:15:13 +0200 Subject: [PATCH 057/100] refactor: add external plugins in their own folder --- package.json | 2 +- src/app.ts | 2 +- src/plugins/README.md | 6 ++---- src/plugins/{env.ts => external/1-env.ts} | 0 src/plugins/{ => external}/cors.ts | 0 src/plugins/{ => external}/helmet.ts | 0 src/plugins/{ => external}/mysql.ts | 7 ++++--- src/plugins/{ => external}/sensible.ts | 0 src/plugins/{ => external}/swagger.ts | 0 src/plugins/{ => external}/under-pressure.ts | 4 +++- 10 files changed, 11 insertions(+), 10 deletions(-) rename src/plugins/{env.ts => external/1-env.ts} (100%) rename src/plugins/{ => external}/cors.ts (100%) rename src/plugins/{ => external}/helmet.ts (100%) rename src/plugins/{ => external}/mysql.ts (80%) rename src/plugins/{ => external}/sensible.ts (100%) rename src/plugins/{ => external}/swagger.ts (100%) rename src/plugins/{ => external}/under-pressure.ts (93%) diff --git a/package.json b/package.json index 96ed8cfc..8216579a 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "start": "npm run build && fastify start -l info dist/app.js", "dev": "npm run build && fastify start -w -l info -P dist/app.js", "standalone": "npm run build && node --env-file=.env dist/server.js", - "lint": "eslint . --ignore-pattern=dist", + "lint": "eslint --ignore-pattern=dist", "lint:fix": "npm run lint -- --fix" }, "keywords": [], diff --git a/src/app.ts b/src/app.ts index ce8f0bbe..e470ed01 100644 --- a/src/app.ts +++ b/src/app.ts @@ -2,8 +2,8 @@ * If you would like to turn your application into a standalone executable, look at server.js file */ -import fastifyAutoload from "@fastify/autoload"; import path from "node:path"; +import fastifyAutoload from "@fastify/autoload"; import { FastifyInstance, FastifyPluginOptions } from "fastify"; export default async function serviceApp( diff --git a/src/plugins/README.md b/src/plugins/README.md index fc72d0b1..1c0af335 100644 --- a/src/plugins/README.md +++ b/src/plugins/README.md @@ -1,10 +1,8 @@ # Plugins Folder -Plugins define behavior that is common to all the routes in your -application. Authentication, caching, templates, and all the other cross cutting concerns should be handled by plugins placed in this folder. +Plugins define behavior that is common to all the routes in your application. Authentication, caching, templates, and all the other cross cutting concerns should be handled by plugins placed in this folder. -Files in this folder are typically defined through the -[`fastify-plugin`](https://github.com/fastify/fastify-plugin module, making them non-encapsulated. They can define decorators and set hooks that will then be used in the rest of your application. +Files in this folder are typically defined through the [`fastify-plugin`](https://github.com/fastify/fastify-plugin) module, making them non-encapsulated. They can define decorators and set hooks that will then be used in the rest of your application. Check out: diff --git a/src/plugins/env.ts b/src/plugins/external/1-env.ts similarity index 100% rename from src/plugins/env.ts rename to src/plugins/external/1-env.ts diff --git a/src/plugins/cors.ts b/src/plugins/external/cors.ts similarity index 100% rename from src/plugins/cors.ts rename to src/plugins/external/cors.ts diff --git a/src/plugins/helmet.ts b/src/plugins/external/helmet.ts similarity index 100% rename from src/plugins/helmet.ts rename to src/plugins/external/helmet.ts diff --git a/src/plugins/mysql.ts b/src/plugins/external/mysql.ts similarity index 80% rename from src/plugins/mysql.ts rename to src/plugins/external/mysql.ts index 9aad7655..c86ec903 100644 --- a/src/plugins/mysql.ts +++ b/src/plugins/external/mysql.ts @@ -17,7 +17,8 @@ export default fp( port: Number(fastify.config.MYSQL_PORT) }); }, - // You should name your plugins if you want to avoid name collisions - // and/or to perform dependency checks. - { name: "db" } + { + // We need to name this plugin to set it as an `under-pressure` dependency + name: "mysql" + } ); diff --git a/src/plugins/sensible.ts b/src/plugins/external/sensible.ts similarity index 100% rename from src/plugins/sensible.ts rename to src/plugins/external/sensible.ts diff --git a/src/plugins/swagger.ts b/src/plugins/external/swagger.ts similarity index 100% rename from src/plugins/swagger.ts rename to src/plugins/external/swagger.ts diff --git a/src/plugins/under-pressure.ts b/src/plugins/external/under-pressure.ts similarity index 93% rename from src/plugins/under-pressure.ts rename to src/plugins/external/under-pressure.ts index 02e59e39..56a21d13 100644 --- a/src/plugins/under-pressure.ts +++ b/src/plugins/external/under-pressure.ts @@ -26,17 +26,19 @@ export default fp( connection = await fastify.mysql.getConnection(); await connection.query("SELECT 1;"); return true; + /* c8 ignore start */ } catch (err) { fastify.log.error(err, "healthCheck has failed"); throw new Error("Database connection is not available"); } finally { connection?.release(); } + /* c8 ignore stop */ }, healthCheckInterval: 5000 }); }, { - dependencies: ["db"] + dependencies: ["mysql"] } ); From a460b7064bf19d37c0dcbd375b1633954c3d0a28 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 09:20:26 +0200 Subject: [PATCH 058/100] docs: highlights the concept of modular monolyth --- src/routes/README.md | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/routes/README.md b/src/routes/README.md index fd8d0b2a..092bd289 100644 --- a/src/routes/README.md +++ b/src/routes/README.md @@ -1,8 +1,11 @@ # Routes Folder -Routes define routes within your application. Fastify provides an -easy path to a microservice architecture, in the future you might want -to independently deploy some of those. +Routes define the pathways within your application. +Fastify's structure supports the modular monolith approach, where your +application is organized into distinct, self-contained modules. +This facilitates easier scaling and future transition to a microservice architecture. +Each module can evolve independently, and in the future, you might want to deploy +some of these modules separately. In this folder you should define all the routes that define the endpoints of your web application. From 7d1a0e9ff51c36c10e7974e911ddf82419971c4b Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 09:30:42 +0200 Subject: [PATCH 059/100] chore: dont wait for MySQL to be ready in ci --- .github/workflows/ci.yml | 7 ------- 1 file changed, 7 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f0da01d4..59e49187 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -51,13 +51,6 @@ jobs: - name: Lint Code run: npm run lint - - name: Wait for MySQL to be ready - run: | - until mysqladmin ping -h localhost -P 3306 --silent; do - echo 'Waiting for MySQL...' - sleep 3 - done - - name: Test env: MYSQL_HOST: localhost From a43ea65d83e6779b650f73a187f849d1d6878a97 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 09:33:37 +0200 Subject: [PATCH 060/100] chore: support only ubuntu-latest os --- .github/workflows/ci.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 59e49187..fe2cd42c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -16,10 +16,9 @@ on: jobs: build: - runs-on: ${{ matrix.os }} + runs-on: ubuntu-latest strategy: matrix: - os: [ubuntu-latest, windows-latest, macos-latest] node-version: [22] services: From 04c1b136ececda6b64dbda17ac14e98790c1d2c1 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 09:49:37 +0200 Subject: [PATCH 061/100] refactor: leverage autoConfig --- src/plugins/external/1-env.ts | 7 ++----- src/plugins/external/cors.ts | 11 +++++------ src/plugins/external/helmet.ts | 12 ++++++------ src/plugins/external/sensible.ts | 11 +++++------ src/plugins/external/swagger.ts | 1 + src/plugins/external/under-pressure.ts | 2 +- src/routes/example/index.ts | 2 +- 7 files changed, 21 insertions(+), 25 deletions(-) diff --git a/src/plugins/external/1-env.ts b/src/plugins/external/1-env.ts index 1d3ee178..c1fa4e3e 100644 --- a/src/plugins/external/1-env.ts +++ b/src/plugins/external/1-env.ts @@ -1,4 +1,3 @@ -import fp from "fastify-plugin"; import env from "@fastify/env"; const schema = { @@ -31,7 +30,7 @@ const schema = { } }; -const options = { +export const autoConfig = { // Decorate Fastify instance with `config` key // Optional, default: 'config' confKey: "config", @@ -57,6 +56,4 @@ const options = { * * @see https://github.com/fastify/fastify-env */ -export default fp(async function (fastify) { - fastify.register(env, options); -}); +export default env diff --git a/src/plugins/external/cors.ts b/src/plugins/external/cors.ts index f19d93fa..1e15b180 100644 --- a/src/plugins/external/cors.ts +++ b/src/plugins/external/cors.ts @@ -1,13 +1,12 @@ -import fp from "fastify-plugin"; import cors from "@fastify/cors"; +export const autoConfig = { + // Set plugin options here +} + /** * This plugins enables the use of CORS. * * @see https://github.com/fastify/fastify-cors */ -export default fp(async function (fastify) { - fastify.register(cors, { - // Set plugin options here - }); -}); +export default cors; \ No newline at end of file diff --git a/src/plugins/external/helmet.ts b/src/plugins/external/helmet.ts index c63e06f9..742726f4 100644 --- a/src/plugins/external/helmet.ts +++ b/src/plugins/external/helmet.ts @@ -1,13 +1,13 @@ -import fp from "fastify-plugin"; import helmet from "@fastify/helmet"; + +export const autoConfig = { + // Set plugin options here +} + /** * This plugins sets the basic security headers. * * @see https://github.com/fastify/fastify-helmet */ -export default fp(async function (fastify) { - fastify.register(helmet, { - // Set plugin options here - }); -}); +export default helmet; diff --git a/src/plugins/external/sensible.ts b/src/plugins/external/sensible.ts index 439ae5f3..d0cd0935 100644 --- a/src/plugins/external/sensible.ts +++ b/src/plugins/external/sensible.ts @@ -1,13 +1,12 @@ -import fp from "fastify-plugin"; import sensible from "@fastify/sensible"; +export const autoConfig = { + // Set plugin options here +} + /** * This plugin adds some utilities to handle http errors * * @see https://github.com/fastify/fastify-sensible */ -export default fp(async function (fastify) { - fastify.register(sensible, { - // Set plugin options here - }); -}); +export default sensible; \ No newline at end of file diff --git a/src/plugins/external/swagger.ts b/src/plugins/external/swagger.ts index 36470a1d..458f11eb 100644 --- a/src/plugins/external/swagger.ts +++ b/src/plugins/external/swagger.ts @@ -2,6 +2,7 @@ import fp from "fastify-plugin"; import fastifySwaggerUi from "@fastify/swagger-ui"; import fastifySwagger from "@fastify/swagger"; + export default fp(async function (fastify) { /** * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas diff --git a/src/plugins/external/under-pressure.ts b/src/plugins/external/under-pressure.ts index 56a21d13..b9a81ce8 100644 --- a/src/plugins/external/under-pressure.ts +++ b/src/plugins/external/under-pressure.ts @@ -16,7 +16,7 @@ export default fp( fastify.register(fastifyUnderPressure, { maxEventLoopDelay: 1000, maxHeapUsedBytes: 100_000_000, - maxRssBytes: 100_000_000, + maxRssBytes: 200_000_000, maxEventLoopUtilization: 0.98, message: "The server is under pressure, retry later!", retryAfter: 50, diff --git a/src/routes/example/index.ts b/src/routes/example/index.ts index db5aace2..35d4e3b3 100644 --- a/src/routes/example/index.ts +++ b/src/routes/example/index.ts @@ -17,7 +17,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { tags: ["Example"] } }, - async function () { + async function () { return { message: "This is an example" }; } ); From 2cd901dbe29741dbe3a56d3297d879c0ac0a5d01 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 09:51:18 +0200 Subject: [PATCH 062/100] fix: add blank line --- src/plugins/external/cors.ts | 2 +- src/plugins/external/sensible.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugins/external/cors.ts b/src/plugins/external/cors.ts index 1e15b180..ab47d26a 100644 --- a/src/plugins/external/cors.ts +++ b/src/plugins/external/cors.ts @@ -9,4 +9,4 @@ export const autoConfig = { * * @see https://github.com/fastify/fastify-cors */ -export default cors; \ No newline at end of file +export default cors; diff --git a/src/plugins/external/sensible.ts b/src/plugins/external/sensible.ts index d0cd0935..9b3109da 100644 --- a/src/plugins/external/sensible.ts +++ b/src/plugins/external/sensible.ts @@ -9,4 +9,4 @@ export const autoConfig = { * * @see https://github.com/fastify/fastify-sensible */ -export default sensible; \ No newline at end of file +export default sensible; From 9993edeb10cb7020029a14adebb2fe5c39c04ddf Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 16:48:56 +0200 Subject: [PATCH 063/100] chore: raise maxRssBytes to 1GB --- src/plugins/external/under-pressure.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/external/under-pressure.ts b/src/plugins/external/under-pressure.ts index b9a81ce8..fc3e7bac 100644 --- a/src/plugins/external/under-pressure.ts +++ b/src/plugins/external/under-pressure.ts @@ -16,7 +16,7 @@ export default fp( fastify.register(fastifyUnderPressure, { maxEventLoopDelay: 1000, maxHeapUsedBytes: 100_000_000, - maxRssBytes: 200_000_000, + maxRssBytes: 1_000_000_000, maxEventLoopUtilization: 0.98, message: "The server is under pressure, retry later!", retryAfter: 50, From c4fac8192bb3eb2204cc255be2d8aba0907974fc Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 11 Jun 2024 16:52:11 +0200 Subject: [PATCH 064/100] chore: dhould not log during tests --- src/server.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/src/server.ts b/src/server.ts index eee1a64f..4293cf99 100644 --- a/src/server.ts +++ b/src/server.ts @@ -33,9 +33,7 @@ function getLoggerOptions() { }; } - // Don't forget to configure it with - // a truthy value in production - return !!process.env.LOGGING; + return { level: process.env.LOG_LEVEL ?? 'silent' }; } const app = Fastify({ From 7110c1364edf661ba925d2234491d2bf6efa4542 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 12 Jun 2024 07:51:39 +0200 Subject: [PATCH 065/100] refactor: launch server with await instead of callback --- src/server.ts | 14 +++++++------- test/routes/body-limit.test.ts | 23 +++++++++++++++++++++++ 2 files changed, 30 insertions(+), 7 deletions(-) create mode 100644 test/routes/body-limit.test.ts diff --git a/src/server.ts b/src/server.ts index 4293cf99..d2080c64 100644 --- a/src/server.ts +++ b/src/server.ts @@ -65,13 +65,13 @@ async function init() { await app.ready(); - // Start listening. - app.listen({ port: process.env.PORT ?? 3000 }, (err) => { - if (err != null) { - app.log.error(err); - process.exit(1); - } - }); + try { + // Start listening. + await app.listen({ port: process.env.PORT ?? 3000 }); + } catch (err) { + app.log.error(err); + process.exit(1); + } } init(); diff --git a/test/routes/body-limit.test.ts b/test/routes/body-limit.test.ts new file mode 100644 index 00000000..a86b4f7a --- /dev/null +++ b/test/routes/body-limit.test.ts @@ -0,0 +1,23 @@ +import { test } from "node:test"; +import { build } from "../helper.js"; + +const largeBody = { name: 'a'.repeat(1024 * 257)}; // Create a string that is 257 KB in size + +test("POST / with body exceeding limit", async (t) => { + const app = await build(t); + + const res = await app.inject({ + method: 'POST', + url: '/example', + payload: largeBody + }); + + console.log(res.statusCode) + +// assert.strictEqual(res.statusCode, 413); // 413 Payload Too Large +// assert.deepStrictEqual(JSON.parse(res.payload), { +// error: "Payload Too Large", +// message: "Request body is too large", +// statusCode: 413 +// }); +}); From 55026826c4e84fc031c00091022c2d7c29797f7f Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 12 Jun 2024 08:02:18 +0200 Subject: [PATCH 066/100] docs: add .env.example file --- .env.example | 14 ++++++++++++++ @types/node/environment.d.ts | 2 +- 2 files changed, 15 insertions(+), 1 deletion(-) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..24edc932 --- /dev/null +++ b/.env.example @@ -0,0 +1,14 @@ +# Must always set to production +# @see https://www.youtube.com/watch?v=HMM7GJC5E2o +NODE_ENV=production + +# Database +MYSQL_HOST: localhost +MYSQL_PORT: 3306 +MYSQL_DATABASE: test_db +MYSQL_USER: test_user +MYSQL_PASSWORD: test_password + +# Server +FASTIFY_CLOSE_GRACE_DELAY=1000 +LOG_LEVEL=info \ No newline at end of file diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index 42ce748d..5ee721bb 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -2,7 +2,7 @@ declare global { namespace NodeJS { interface ProcessEnv { PORT: number; - LOGGING?: number; + LOG_LEVEL: string; FASTIFY_CLOSE_GRACE_DELAY: number; } } From 66f3664e5d4ad47bec61891fdad8f23562674479 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 17 Jun 2024 17:36:17 +0200 Subject: [PATCH 067/100] fix: remove body-limit test --- test/routes/body-limit.test.ts | 23 ----------------------- 1 file changed, 23 deletions(-) delete mode 100644 test/routes/body-limit.test.ts diff --git a/test/routes/body-limit.test.ts b/test/routes/body-limit.test.ts deleted file mode 100644 index a86b4f7a..00000000 --- a/test/routes/body-limit.test.ts +++ /dev/null @@ -1,23 +0,0 @@ -import { test } from "node:test"; -import { build } from "../helper.js"; - -const largeBody = { name: 'a'.repeat(1024 * 257)}; // Create a string that is 257 KB in size - -test("POST / with body exceeding limit", async (t) => { - const app = await build(t); - - const res = await app.inject({ - method: 'POST', - url: '/example', - payload: largeBody - }); - - console.log(res.statusCode) - -// assert.strictEqual(res.statusCode, 413); // 413 Payload Too Large -// assert.deepStrictEqual(JSON.parse(res.payload), { -// error: "Payload Too Large", -// message: "Request body is too large", -// statusCode: 413 -// }); -}); From f88fd1c3c81e23d715a92ab072959a47ae33e3c5 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 17 Jun 2024 17:48:57 +0200 Subject: [PATCH 068/100] refactor: leverage autoConfig callback feature --- package.json | 2 +- src/plugins/external/mysql.ts | 35 +++++++--------- src/plugins/external/under-pressure.ts | 55 +++++++++++++------------- 3 files changed, 44 insertions(+), 48 deletions(-) diff --git a/package.json b/package.json index 8216579a..a197a7bc 100644 --- a/package.json +++ b/package.json @@ -21,7 +21,7 @@ "author": "Michelet Jean ", "license": "MIT", "dependencies": { - "@fastify/autoload": "^5.0.0", + "@fastify/autoload": "^5.10.0", "@fastify/cors": "^9.0.1", "@fastify/env": "^4.3.0", "@fastify/helmet": "^11.1.1", diff --git a/src/plugins/external/mysql.ts b/src/plugins/external/mysql.ts index c86ec903..cf50375f 100644 --- a/src/plugins/external/mysql.ts +++ b/src/plugins/external/mysql.ts @@ -1,24 +1,19 @@ import fp from "fastify-plugin"; import fastifyMysql from "@fastify/mysql"; +import { FastifyInstance } from "fastify"; -/** - * This plugin allows using `mysql2` with Fastify. - * - * @see https://github.com/fastify/fastify-mysql - */ -export default fp( - async function (fastify) { - fastify.register(fastifyMysql, { - promise: true, - host: fastify.config.MYSQL_HOST, - user: fastify.config.MYSQL_USER, - password: fastify.config.MYSQL_PASSWORD, - database: fastify.config.MYSQL_DATABASE, - port: Number(fastify.config.MYSQL_PORT) - }); - }, - { - // We need to name this plugin to set it as an `under-pressure` dependency - name: "mysql" +export const autoConfig = (fastify: FastifyInstance) => { + return { + promise: true, + host: fastify.config.MYSQL_HOST, + user: fastify.config.MYSQL_USER, + password: fastify.config.MYSQL_PASSWORD, + database: fastify.config.MYSQL_DATABASE, + port: Number(fastify.config.MYSQL_PORT) } -); +} + +export default fp(fastifyMysql, { + name: 'mysql' +}) + diff --git a/src/plugins/external/under-pressure.ts b/src/plugins/external/under-pressure.ts index fc3e7bac..7f02020e 100644 --- a/src/plugins/external/under-pressure.ts +++ b/src/plugins/external/under-pressure.ts @@ -2,6 +2,33 @@ import { FastifyInstance } from "fastify"; import fastifyUnderPressure from "@fastify/under-pressure"; import fp from "fastify-plugin"; +export const autoConfig = (fastify: FastifyInstance) => { + return { + maxEventLoopDelay: 1000, + maxHeapUsedBytes: 100_000_000, + maxRssBytes: 1_000_000_000, + maxEventLoopUtilization: 0.98, + message: "The server is under pressure, retry later!", + retryAfter: 50, + healthCheck: async () => { + let connection; + try { + connection = await fastify.mysql.getConnection(); + await connection.query("SELECT 1;"); + return true; + /* c8 ignore start */ + } catch (err) { + fastify.log.error(err, "healthCheck has failed"); + throw new Error("Database connection is not available"); + } finally { + connection?.release(); + } + /* c8 ignore stop */ + }, + healthCheckInterval: 5000 + } +} + /** * A Fastify plugin for mesuring process load and automatically * handle of "Service Unavailable" @@ -11,33 +38,7 @@ import fp from "fastify-plugin"; * Video on the topic: Do not thrash the event loop * @see https://www.youtube.com/watch?v=VI29mUA8n9w */ -export default fp( - async function (fastify: FastifyInstance) { - fastify.register(fastifyUnderPressure, { - maxEventLoopDelay: 1000, - maxHeapUsedBytes: 100_000_000, - maxRssBytes: 1_000_000_000, - maxEventLoopUtilization: 0.98, - message: "The server is under pressure, retry later!", - retryAfter: 50, - healthCheck: async () => { - let connection; - try { - connection = await fastify.mysql.getConnection(); - await connection.query("SELECT 1;"); - return true; - /* c8 ignore start */ - } catch (err) { - fastify.log.error(err, "healthCheck has failed"); - throw new Error("Database connection is not available"); - } finally { - connection?.release(); - } - /* c8 ignore stop */ - }, - healthCheckInterval: 5000 - }); - }, +export default fp(fastifyUnderPressure, { dependencies: ["mysql"] } From 5529b8d132892a2a6ca17eb289b38b2738b574de Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 17 Jun 2024 17:54:05 +0200 Subject: [PATCH 069/100] fix: add blank line --- .env.example | 2 +- src/plugins/external/mysql.ts | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.env.example b/.env.example index 24edc932..2b74dfed 100644 --- a/.env.example +++ b/.env.example @@ -11,4 +11,4 @@ MYSQL_PASSWORD: test_password # Server FASTIFY_CLOSE_GRACE_DELAY=1000 -LOG_LEVEL=info \ No newline at end of file +LOG_LEVEL=info diff --git a/src/plugins/external/mysql.ts b/src/plugins/external/mysql.ts index cf50375f..9f5a4c42 100644 --- a/src/plugins/external/mysql.ts +++ b/src/plugins/external/mysql.ts @@ -16,4 +16,3 @@ export const autoConfig = (fastify: FastifyInstance) => { export default fp(fastifyMysql, { name: 'mysql' }) - From 0ea8f263d3af9777ba2b371bd59c55dbea0fcca9 Mon Sep 17 00:00:00 2001 From: jean Date: Mon, 17 Jun 2024 17:56:07 +0200 Subject: [PATCH 070/100] chore: remove build command before starting server related scripts --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index a197a7bc..a03a6cbb 100644 --- a/package.json +++ b/package.json @@ -11,9 +11,9 @@ "build": "rm -rf dist && tsc", "watch": "npm run build -- --watch", "test": "tap test/**/*", - "start": "npm run build && fastify start -l info dist/app.js", - "dev": "npm run build && fastify start -w -l info -P dist/app.js", - "standalone": "npm run build && node --env-file=.env dist/server.js", + "start": "fastify start -l info dist/app.js", + "dev": "fastify start -w -l info -P dist/app.js", + "standalone": "node --env-file=.env dist/server.js", "lint": "eslint --ignore-pattern=dist", "lint:fix": "npm run lint -- --fix" }, From 3703d878c9a619b50a8bdd78d1fd44cf4561362e Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 18 Jun 2024 09:47:11 +0200 Subject: [PATCH 071/100] feat: add basic jwt authentication --- .env.example | 3 + @types/fastify/index.d.ts | 1 + package.json | 1 + src/app.ts | 9 ++- src/plugins/external/1-env.ts | 11 +++- src/plugins/external/cors.ts | 2 +- src/plugins/external/helmet.ts | 3 +- src/plugins/external/jwt.ts | 10 +++ src/plugins/external/mysql.ts | 8 +-- src/plugins/external/sensible.ts | 2 +- src/plugins/external/swagger.ts | 2 +- src/plugins/external/under-pressure.ts | 12 ++-- src/routes/README.md | 10 +-- src/routes/api/auth/index.ts | 40 ++++++++++++ src/routes/api/autohooks.ts | 9 +++ src/routes/api/index.ts | 10 +++ src/routes/api/tasks/index.ts | 24 +++++++ src/routes/example/index.ts | 48 -------------- src/schemas/auth.ts | 6 ++ src/schemas/tasks.ts | 6 ++ src/server.ts | 2 +- test/{routes => app}/error-handler.test.ts | 0 .../{routes => app}/not-found-handler.test.ts | 0 test/helper.ts | 26 ++++++++ test/routes/api/api.test.ts | 41 ++++++++++++ test/routes/api/auth/auth.test.ts | 63 +++++++++++++++++++ test/routes/api/tasks/tasks.test.ts | 17 +++++ test/routes/example/example.test.ts | 58 ----------------- 28 files changed, 293 insertions(+), 131 deletions(-) create mode 100644 src/plugins/external/jwt.ts create mode 100644 src/routes/api/auth/index.ts create mode 100644 src/routes/api/autohooks.ts create mode 100644 src/routes/api/index.ts create mode 100644 src/routes/api/tasks/index.ts delete mode 100644 src/routes/example/index.ts create mode 100644 src/schemas/auth.ts create mode 100644 src/schemas/tasks.ts rename test/{routes => app}/error-handler.test.ts (100%) rename test/{routes => app}/not-found-handler.test.ts (100%) create mode 100644 test/routes/api/api.test.ts create mode 100644 test/routes/api/auth/auth.test.ts create mode 100644 test/routes/api/tasks/tasks.test.ts delete mode 100644 test/routes/example/example.test.ts diff --git a/.env.example b/.env.example index 2b74dfed..184e85a7 100644 --- a/.env.example +++ b/.env.example @@ -12,3 +12,6 @@ MYSQL_PASSWORD: test_password # Server FASTIFY_CLOSE_GRACE_DELAY=1000 LOG_LEVEL=info + +# Security +JWT_SECRET= diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts index 6761fb8c..9fc8ac19 100644 --- a/@types/fastify/index.d.ts +++ b/@types/fastify/index.d.ts @@ -11,6 +11,7 @@ declare module "fastify" { MYSQL_USER: string; MYSQL_PASSWORD: string; MYSQL_DATABASE: string; + JWT_SECRET: string; }; } } diff --git a/package.json b/package.json index a03a6cbb..a15933fe 100644 --- a/package.json +++ b/package.json @@ -25,6 +25,7 @@ "@fastify/cors": "^9.0.1", "@fastify/env": "^4.3.0", "@fastify/helmet": "^11.1.1", + "@fastify/jwt": "^8.0.1", "@fastify/mysql": "^4.3.0", "@fastify/sensible": "^5.0.0", "@fastify/swagger": "^8.14.0", diff --git a/src/app.ts b/src/app.ts index e470ed01..2dc96ca4 100644 --- a/src/app.ts +++ b/src/app.ts @@ -22,6 +22,8 @@ export default async function serviceApp( // define your routes in one of these fastify.register(fastifyAutoload, { dir: path.join(import.meta.dirname, "routes"), + autoHooks: true, + cascadeHooks: true, options: { ...opts } }); @@ -41,7 +43,12 @@ export default async function serviceApp( reply.code(err.statusCode ?? 500); - return { message: "Internal Server Error" }; + let message = "Internal Server Error"; + if (err.statusCode === 401) { + message = err.message; + } + + return { message }; }); fastify.setNotFoundHandler((request, reply) => { diff --git a/src/plugins/external/1-env.ts b/src/plugins/external/1-env.ts index c1fa4e3e..22facc3f 100644 --- a/src/plugins/external/1-env.ts +++ b/src/plugins/external/1-env.ts @@ -7,9 +7,11 @@ const schema = { "MYSQL_PORT", "MYSQL_USER", "MYSQL_PASSWORD", - "MYSQL_DATABASE" + "MYSQL_DATABASE", + "JWT_SECRET" ], properties: { + // Database MYSQL_HOST: { type: "string", default: "localhost" @@ -26,6 +28,11 @@ const schema = { }, MYSQL_DATABASE: { type: "string" + }, + + // Security + JWT_SECRET: { + type: "string" } } }; @@ -56,4 +63,4 @@ export const autoConfig = { * * @see https://github.com/fastify/fastify-env */ -export default env +export default env; diff --git a/src/plugins/external/cors.ts b/src/plugins/external/cors.ts index ab47d26a..26e9312d 100644 --- a/src/plugins/external/cors.ts +++ b/src/plugins/external/cors.ts @@ -2,7 +2,7 @@ import cors from "@fastify/cors"; export const autoConfig = { // Set plugin options here -} +}; /** * This plugins enables the use of CORS. diff --git a/src/plugins/external/helmet.ts b/src/plugins/external/helmet.ts index 742726f4..9f7a3b6a 100644 --- a/src/plugins/external/helmet.ts +++ b/src/plugins/external/helmet.ts @@ -1,9 +1,8 @@ import helmet from "@fastify/helmet"; - export const autoConfig = { // Set plugin options here -} +}; /** * This plugins sets the basic security headers. diff --git a/src/plugins/external/jwt.ts b/src/plugins/external/jwt.ts new file mode 100644 index 00000000..60f4e34e --- /dev/null +++ b/src/plugins/external/jwt.ts @@ -0,0 +1,10 @@ +import fastifyJwt from "@fastify/jwt"; +import { FastifyInstance } from "fastify"; + +export const autoConfig = (fastify: FastifyInstance) => { + return { + secret: fastify.config.JWT_SECRET + }; +}; + +export default fastifyJwt; diff --git a/src/plugins/external/mysql.ts b/src/plugins/external/mysql.ts index 9f5a4c42..e1ff5fe9 100644 --- a/src/plugins/external/mysql.ts +++ b/src/plugins/external/mysql.ts @@ -10,9 +10,9 @@ export const autoConfig = (fastify: FastifyInstance) => { password: fastify.config.MYSQL_PASSWORD, database: fastify.config.MYSQL_DATABASE, port: Number(fastify.config.MYSQL_PORT) - } -} + }; +}; export default fp(fastifyMysql, { - name: 'mysql' -}) + name: "mysql" +}); diff --git a/src/plugins/external/sensible.ts b/src/plugins/external/sensible.ts index 9b3109da..296e4d57 100644 --- a/src/plugins/external/sensible.ts +++ b/src/plugins/external/sensible.ts @@ -2,7 +2,7 @@ import sensible from "@fastify/sensible"; export const autoConfig = { // Set plugin options here -} +}; /** * This plugin adds some utilities to handle http errors diff --git a/src/plugins/external/swagger.ts b/src/plugins/external/swagger.ts index 458f11eb..f544d2f1 100644 --- a/src/plugins/external/swagger.ts +++ b/src/plugins/external/swagger.ts @@ -2,7 +2,6 @@ import fp from "fastify-plugin"; import fastifySwaggerUi from "@fastify/swagger-ui"; import fastifySwagger from "@fastify/swagger"; - export default fp(async function (fastify) { /** * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas @@ -10,6 +9,7 @@ export default fp(async function (fastify) { * @see https://github.com/fastify/fastify-swagger */ await fastify.register(fastifySwagger, { + hideUntagged: true, openapi: { info: { title: "Fastify demo API", diff --git a/src/plugins/external/under-pressure.ts b/src/plugins/external/under-pressure.ts index 7f02020e..f8213fc0 100644 --- a/src/plugins/external/under-pressure.ts +++ b/src/plugins/external/under-pressure.ts @@ -26,8 +26,8 @@ export const autoConfig = (fastify: FastifyInstance) => { /* c8 ignore stop */ }, healthCheckInterval: 5000 - } -} + }; +}; /** * A Fastify plugin for mesuring process load and automatically @@ -38,8 +38,6 @@ export const autoConfig = (fastify: FastifyInstance) => { * Video on the topic: Do not thrash the event loop * @see https://www.youtube.com/watch?v=VI29mUA8n9w */ -export default fp(fastifyUnderPressure, - { - dependencies: ["mysql"] - } -); +export default fp(fastifyUnderPressure, { + dependencies: ["mysql"] +}); diff --git a/src/routes/README.md b/src/routes/README.md index 092bd289..0d4c6812 100644 --- a/src/routes/README.md +++ b/src/routes/README.md @@ -1,10 +1,10 @@ # Routes Folder -Routes define the pathways within your application. -Fastify's structure supports the modular monolith approach, where your -application is organized into distinct, self-contained modules. -This facilitates easier scaling and future transition to a microservice architecture. -Each module can evolve independently, and in the future, you might want to deploy +Routes define the pathways within your application. +Fastify's structure supports the modular monolith approach, where your +application is organized into distinct, self-contained modules. +This facilitates easier scaling and future transition to a microservice architecture. +Each module can evolve independently, and in the future, you might want to deploy some of these modules separately. In this folder you should define all the routes that define the endpoints diff --git a/src/routes/api/auth/index.ts b/src/routes/api/auth/index.ts new file mode 100644 index 00000000..8276ed4c --- /dev/null +++ b/src/routes/api/auth/index.ts @@ -0,0 +1,40 @@ +import { + FastifyPluginAsyncTypebox, + Type +} from "@fastify/type-provider-typebox"; +import { CredentialsSchema } from "../../../schemas/auth.js"; + +const plugin: FastifyPluginAsyncTypebox = async (fastify) => { + fastify.post( + "/login", + { + schema: { + body: CredentialsSchema, + response: { + 200: Type.Object({ + token: Type.String() + }), + 401: Type.Object({ + message: Type.String() + }) + }, + tags: ["Authentication"] + } + }, + async function (request, reply) { + const { username, password } = request.body; + + if (username === "basic" && password === "password") { + const token = fastify.jwt.sign({ username }); + + return { token }; + } + + reply.status(401); + + return { message: "Invalid username or password." }; + } + ); +}; + +export default plugin; diff --git a/src/routes/api/autohooks.ts b/src/routes/api/autohooks.ts new file mode 100644 index 00000000..39951f42 --- /dev/null +++ b/src/routes/api/autohooks.ts @@ -0,0 +1,9 @@ +import { FastifyInstance } from "fastify"; + +export default async function (fastify: FastifyInstance) { + fastify.addHook("onRequest", async (request) => { + if (!request.url.startsWith("/api/auth/login")) { + await request.jwtVerify(); + } + }); +} diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts new file mode 100644 index 00000000..236595a4 --- /dev/null +++ b/src/routes/api/index.ts @@ -0,0 +1,10 @@ +import { FastifyInstance } from "fastify"; + +export default async function (fastify: FastifyInstance) { + fastify.get("/", ({ protocol, hostname }) => { + return { + message: + "See documentation at " + `${protocol}://${hostname}/documentation` + }; + }); +} diff --git a/src/routes/api/tasks/index.ts b/src/routes/api/tasks/index.ts new file mode 100644 index 00000000..2d8f29b5 --- /dev/null +++ b/src/routes/api/tasks/index.ts @@ -0,0 +1,24 @@ +import { + FastifyPluginAsyncTypebox, + Type +} from "@fastify/type-provider-typebox"; +import { TaskSchema } from "../../../schemas/tasks.js"; + +const plugin: FastifyPluginAsyncTypebox = async (fastify) => { + fastify.get( + "/", + { + schema: { + response: { + 200: Type.Array(TaskSchema) + }, + tags: ["Tasks"] + } + }, + async function () { + return [{ id: 1, name: "Do something..." }]; + } + ); +}; + +export default plugin; diff --git a/src/routes/example/index.ts b/src/routes/example/index.ts deleted file mode 100644 index 35d4e3b3..00000000 --- a/src/routes/example/index.ts +++ /dev/null @@ -1,48 +0,0 @@ -import { - FastifyPluginAsyncTypebox, - Type -} from "@fastify/type-provider-typebox"; - -const plugin: FastifyPluginAsyncTypebox = async (fastify) => { - // Prefixed with /example by the autoloader - fastify.get( - "/", - { - schema: { - response: { - 200: Type.Object({ - message: Type.String() - }) - }, - tags: ["Example"] - } - }, - async function () { - return { message: "This is an example" }; - } - ); - - fastify.post( - "/", - { - schema: { - body: Type.Object({ - digit: Type.Number({ minimum: 0, maximum: 9 }) - }), - response: { - 200: Type.Object({ - message: Type.String() - }) - }, - tags: ["Example"] - } - }, - async function (req) { - const { digit } = req.body; - - return { message: `Here is the digit you sent: ${digit}` }; - } - ); -}; - -export default plugin; diff --git a/src/schemas/auth.ts b/src/schemas/auth.ts new file mode 100644 index 00000000..9d67c5a9 --- /dev/null +++ b/src/schemas/auth.ts @@ -0,0 +1,6 @@ +import { Type } from "@sinclair/typebox"; + +export const CredentialsSchema = Type.Object({ + username: Type.String(), + password: Type.String() +}); diff --git a/src/schemas/tasks.ts b/src/schemas/tasks.ts new file mode 100644 index 00000000..6f506c5c --- /dev/null +++ b/src/schemas/tasks.ts @@ -0,0 +1,6 @@ +import { Type } from "@sinclair/typebox"; + +export const TaskSchema = Type.Object({ + id: Type.Number(), + name: Type.String() +}); diff --git a/src/server.ts b/src/server.ts index d2080c64..e2b8673e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -33,7 +33,7 @@ function getLoggerOptions() { }; } - return { level: process.env.LOG_LEVEL ?? 'silent' }; + return { level: process.env.LOG_LEVEL ?? "silent" }; } const app = Fastify({ diff --git a/test/routes/error-handler.test.ts b/test/app/error-handler.test.ts similarity index 100% rename from test/routes/error-handler.test.ts rename to test/app/error-handler.test.ts diff --git a/test/routes/not-found-handler.test.ts b/test/app/not-found-handler.test.ts similarity index 100% rename from test/routes/not-found-handler.test.ts rename to test/app/not-found-handler.test.ts diff --git a/test/helper.ts b/test/helper.ts index 98a8e977..ce16fe95 100644 --- a/test/helper.ts +++ b/test/helper.ts @@ -1,6 +1,7 @@ // This file contains code that we reuse // between our tests. +import { InjectOptions } from "fastify"; import { build as buildApplication } from "fastify-cli/helper.js"; import path from "node:path"; import { TestContext } from "node:test"; @@ -13,6 +14,20 @@ export function config() { return {}; } +// We will create different users with different roles +async function login(username: string) { + const res = await this.inject({ + method: "POST", + url: "/api/auth/login", + payload: { + username, + password: "password" + } + }); + + return JSON.parse(res.payload).token; +} + // automatically build and tear down our instance export async function build(t: TestContext) { // you can set all the options supported by the fastify CLI command @@ -23,6 +38,17 @@ export async function build(t: TestContext) { // different from the production setup const app = await buildApplication(argv, config()); + app.login = login; + + app.injectWithLogin = async (username: string, opts: InjectOptions) => { + opts.headers = { + ...opts.headers, + Authorization: `Bearer ${await app.login(username)}` + }; + + return app.inject(opts); + }; + // close the app after we are done t.after(() => app.close()); diff --git a/test/routes/api/api.test.ts b/test/routes/api/api.test.ts new file mode 100644 index 00000000..36328ace --- /dev/null +++ b/test/routes/api/api.test.ts @@ -0,0 +1,41 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { build } from "../../helper.js"; + +test("GET /api without authorization header", async (t) => { + const app = await build(t); + + const res = await app.inject({ + url: "/api" + }); + + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "No Authorization was found in request.headers" + }); +}); + +test("GET /api without JWT Token", async (t) => { + const app = await build(t); + + const res = await app.inject({ + method: "GET", + url: "/api", + headers: { + Authorization: "Bearer invalidtoken" + } + }); + + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "Authorization token is invalid: The token is malformed." + }); +}); + +test("GET /api with JWT Token", async (t) => { + const app = await build(t); + + const res = await app.injectWithLogin("basic", { + url: "/api" + }); + + assert.equal(res.statusCode, 200); +}); diff --git a/test/routes/api/auth/auth.test.ts b/test/routes/api/auth/auth.test.ts new file mode 100644 index 00000000..33714089 --- /dev/null +++ b/test/routes/api/auth/auth.test.ts @@ -0,0 +1,63 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { build } from "../../../helper.js"; + +test("POST /api/auth/login with valid credentials", async (t) => { + const app = await build(t); + + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { + username: "basic", + password: "password" + } + }); + + assert.strictEqual(res.statusCode, 200); + const responsePayload = JSON.parse(res.payload); + assert.ok(responsePayload.token, "Token should be present in the response"); +}); + +test("POST /api/auth/login with invalid credentials", async (t) => { + const app = await build(t); + + const testCases = [ + { + username: "invalid_user", + password: "password", + description: "invalid username" + }, + { + username: "basic", + password: "wrong_password", + description: "invalid password" + }, + { + username: "invalid_user", + password: "wrong_password", + description: "both invalid" + } + ]; + + for (const testCase of testCases) { + const res = await app.inject({ + method: "POST", + url: "/api/auth/login", + payload: { + username: testCase.username, + password: testCase.password + } + }); + + assert.strictEqual( + res.statusCode, + 401, + `Failed for case: ${testCase.description}` + ); + + assert.deepStrictEqual(JSON.parse(res.payload), { + message: "Invalid username or password." + }); + } +}); diff --git a/test/routes/api/tasks/tasks.test.ts b/test/routes/api/tasks/tasks.test.ts new file mode 100644 index 00000000..8dc2d33e --- /dev/null +++ b/test/routes/api/tasks/tasks.test.ts @@ -0,0 +1,17 @@ +import { test } from "node:test"; +import assert from "node:assert"; +import { build } from "../../../helper.js"; + +test("GET /api/tasks with valid JWT Token should return 200", async (t) => { + const app = await build(t); + + const res = await app.injectWithLogin("basic", { + method: "GET", + url: "/api/tasks" + }); + + assert.strictEqual(res.statusCode, 200); + assert.deepStrictEqual(JSON.parse(res.payload), [ + { id: 1, name: "Do something..." } + ]); +}); diff --git a/test/routes/example/example.test.ts b/test/routes/example/example.test.ts deleted file mode 100644 index 7c8546ef..00000000 --- a/test/routes/example/example.test.ts +++ /dev/null @@ -1,58 +0,0 @@ -import { test } from "node:test"; -import assert from "node:assert"; -import { build } from "../../helper.js"; - -test("GET /example", async (t) => { - const app = await build(t); - - const res = await app.inject({ - url: "/example" - }); - - assert.deepStrictEqual(JSON.parse(res.payload), { - message: "This is an example" - }); - - app.listen(); -}); - -test("POST /example with valid body", async (t) => { - const app = await build(t); - - const res = await app.inject({ - method: "POST", - url: "/example", - payload: { - digit: 5 - } - }); - - assert.strictEqual(res.statusCode, 200); - assert.deepStrictEqual(JSON.parse(res.payload), { - message: "Here is the digit you sent: 5" - }); -}); - -test("POST /example with invalid body", async (t) => { - const app = await build(t); - - const testCases = [ - { digit: 10, description: "too high" }, - { digit: -1, description: "too low" }, - { digit: "a", description: "not a number" } - ]; - - for (const testCase of testCases) { - const res = await app.inject({ - method: "POST", - url: "/example", - payload: { digit: testCase.digit } - }); - - assert.strictEqual( - res.statusCode, - 400, - `Failed for case: ${testCase.description}` - ); - } -}); From 02ffde12ab2e1bf1a70e99466c9bcb7772bb1e20 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 18 Jun 2024 10:27:06 +0200 Subject: [PATCH 072/100] refactor: return username in /api response --- @types/fastify/index.d.ts | 6 ++++++ src/routes/api/index.ts | 4 ++-- test/routes/api/api.test.ts | 1 + test/routes/api/auth/auth.test.ts | 3 +-- 4 files changed, 10 insertions(+), 4 deletions(-) diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts index 9fc8ac19..4382db67 100644 --- a/@types/fastify/index.d.ts +++ b/@types/fastify/index.d.ts @@ -1,4 +1,6 @@ import { MySQLPromisePool } from "@fastify/mysql"; +import { Static } from "@sinclair/typebox"; +import { CredentialsSchema } from "../../src/schemas/auth.ts"; declare module "fastify" { export interface FastifyInstance { @@ -14,6 +16,10 @@ declare module "fastify" { JWT_SECRET: string; }; } + + export interface FastifyRequest { + user: Static + } } export {}; diff --git a/src/routes/api/index.ts b/src/routes/api/index.ts index 236595a4..71cc0462 100644 --- a/src/routes/api/index.ts +++ b/src/routes/api/index.ts @@ -1,10 +1,10 @@ import { FastifyInstance } from "fastify"; export default async function (fastify: FastifyInstance) { - fastify.get("/", ({ protocol, hostname }) => { + fastify.get("/", ({ user, protocol, hostname }) => { return { message: - "See documentation at " + `${protocol}://${hostname}/documentation` + `Hello ${user.username}! See documentation at ${protocol}://${hostname}/documentation` }; }); } diff --git a/test/routes/api/api.test.ts b/test/routes/api/api.test.ts index 36328ace..b41ac46f 100644 --- a/test/routes/api/api.test.ts +++ b/test/routes/api/api.test.ts @@ -38,4 +38,5 @@ test("GET /api with JWT Token", async (t) => { }); assert.equal(res.statusCode, 200); + assert.ok(JSON.parse(res.payload).message.startsWith("Hello basic!")); }); diff --git a/test/routes/api/auth/auth.test.ts b/test/routes/api/auth/auth.test.ts index 33714089..fbb2f725 100644 --- a/test/routes/api/auth/auth.test.ts +++ b/test/routes/api/auth/auth.test.ts @@ -15,8 +15,7 @@ test("POST /api/auth/login with valid credentials", async (t) => { }); assert.strictEqual(res.statusCode, 200); - const responsePayload = JSON.parse(res.payload); - assert.ok(responsePayload.token, "Token should be present in the response"); + assert.ok(JSON.parse(res.payload).token); }); test("POST /api/auth/login with invalid credentials", async (t) => { From fb82c1e23f5dc7118d08d21f746ed7b30e8ad755 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 18 Jun 2024 11:50:44 +0200 Subject: [PATCH 073/100] chore: use of postgrator for db migrations --- migrations/001.do.users.sql | 7 ++++++ migrations/001.undo.users.sql | 1 + migrations/002.do.tasks.sql | 11 +++++++++ migrations/002.undo.tasks.sql | 1 + migrations/003.do.user_tasks.sql | 7 ++++++ migrations/003.undo.user_tasks.sql | 1 + package.json | 6 +++-- scripts/migrate.js | 39 ++++++++++++++++++++++++++++++ 8 files changed, 71 insertions(+), 2 deletions(-) create mode 100644 migrations/001.do.users.sql create mode 100644 migrations/001.undo.users.sql create mode 100644 migrations/002.do.tasks.sql create mode 100644 migrations/002.undo.tasks.sql create mode 100644 migrations/003.do.user_tasks.sql create mode 100644 migrations/003.undo.user_tasks.sql create mode 100644 scripts/migrate.js diff --git a/migrations/001.do.users.sql b/migrations/001.do.users.sql new file mode 100644 index 00000000..66dcd900 --- /dev/null +++ b/migrations/001.do.users.sql @@ -0,0 +1,7 @@ +CREATE TABLE users ( + id INT AUTO_INCREMENT PRIMARY KEY, + username VARCHAR(255) NOT NULL, + password VARCHAR(255) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP +); diff --git a/migrations/001.undo.users.sql b/migrations/001.undo.users.sql new file mode 100644 index 00000000..365a2107 --- /dev/null +++ b/migrations/001.undo.users.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS users; \ No newline at end of file diff --git a/migrations/002.do.tasks.sql b/migrations/002.do.tasks.sql new file mode 100644 index 00000000..9c08a4e2 --- /dev/null +++ b/migrations/002.do.tasks.sql @@ -0,0 +1,11 @@ +CREATE TABLE tasks ( + id INT AUTO_INCREMENT PRIMARY KEY, + name VARCHAR(255) NOT NULL, + author_id INT NOT NULL, + assigned_user_id INT, + status VARCHAR(50) NOT NULL, + created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP, + updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, + FOREIGN KEY (author_id) REFERENCES users(id), + FOREIGN KEY (assigned_user_id) REFERENCES users(id) +); diff --git a/migrations/002.undo.tasks.sql b/migrations/002.undo.tasks.sql new file mode 100644 index 00000000..ddcb09ad --- /dev/null +++ b/migrations/002.undo.tasks.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS tasks; \ No newline at end of file diff --git a/migrations/003.do.user_tasks.sql b/migrations/003.do.user_tasks.sql new file mode 100644 index 00000000..6abc7512 --- /dev/null +++ b/migrations/003.do.user_tasks.sql @@ -0,0 +1,7 @@ +CREATE TABLE user_tasks ( + user_id INT NOT NULL, + task_id INT NOT NULL, + PRIMARY KEY (user_id, task_id), + FOREIGN KEY (user_id) REFERENCES users(id), + FOREIGN KEY (task_id) REFERENCES tasks(id) +); diff --git a/migrations/003.undo.user_tasks.sql b/migrations/003.undo.user_tasks.sql new file mode 100644 index 00000000..85d66c69 --- /dev/null +++ b/migrations/003.undo.user_tasks.sql @@ -0,0 +1 @@ +DROP TABLE IF EXISTS user_tasks; \ No newline at end of file diff --git a/package.json b/package.json index a15933fe..f05bf662 100644 --- a/package.json +++ b/package.json @@ -15,7 +15,8 @@ "dev": "fastify start -w -l info -P dist/app.js", "standalone": "node --env-file=.env dist/server.js", "lint": "eslint --ignore-pattern=dist", - "lint:fix": "npm run lint -- --fix" + "lint:fix": "npm run lint -- --fix", + "migrate": "node --env-file=.env scripts/migrate.js" }, "keywords": [], "author": "Michelet Jean ", @@ -35,7 +36,8 @@ "@sinclair/typebox": "^0.32.31", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", - "fastify-plugin": "^4.0.0" + "fastify-plugin": "^4.0.0", + "postgrator": "^7.2.0" }, "devDependencies": { "@types/node": "^20.14.2", diff --git a/scripts/migrate.js b/scripts/migrate.js new file mode 100644 index 00000000..1eab2ca4 --- /dev/null +++ b/scripts/migrate.js @@ -0,0 +1,39 @@ +import mysql from 'mysql2/promise' +import path from 'path' +import Postgrator from 'postgrator' + +async function doMigration () { + const connection = await mysql.createConnection({ + multipleStatements: true, + host: process.env.MYSQL_HOST, + port: process.env.MYSQL_PORT, + database: process.env.MYSQL_DATABASE, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD + }) + + const postgrator = new Postgrator({ + migrationPattern: path.join(import.meta.dirname, '../migrations', '*'), + driver: 'mysql', + database: process.env.MYSQL_DATABASE, + execQuery: async (query) => { + const [rows, fields] = await connection.query(query) + + return { rows, fields } + }, + schemaTable: 'schemaversion' + }) + + await postgrator.migrate() + + await new Promise((resolve, reject) => { + connection.end((err) => { + if (err) { + return reject(err) + } + resolve() + }) + }) +} + +doMigration().catch(err => console.error(err)) From 256bb38127a5c497805b2e97f8e24ebd87f92292 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 18 Jun 2024 12:01:53 +0200 Subject: [PATCH 074/100] chore: add JWT_SECRET generation step for CI env --- .github/workflows/ci.yml | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fe2cd42c..5be190d1 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -50,6 +50,12 @@ jobs: - name: Lint Code run: npm run lint + - name: Generate JWT Secret + id: gen-jwt + run: | + JWT_SECRET=$(openssl rand -hex 32) + echo "JWT_SECRET=$JWT_SECRET" >> $GITHUB_ENV + - name: Test env: MYSQL_HOST: localhost @@ -57,4 +63,5 @@ jobs: MYSQL_DATABASE: test_db MYSQL_USER: test_user MYSQL_PASSWORD: test_password + # JWT_SECRET is dynamically generated and loaded from the environment run: npm run test From a15de0b50029599e3ef7b63ae83643c1fc75927b Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 22 Jun 2024 12:33:53 +0200 Subject: [PATCH 075/100] feat: create repository plugin to simplify queries --- @types/fastify/index.d.ts | 3 +- package.json | 8 ++- scripts/migrate.js | 4 +- scripts/seed-database.js | 60 ++++++++++++++++++ src/app.ts | 11 +++- src/plugins/custom/repository.ts | 103 +++++++++++++++++++++++++++++++ src/plugins/support.ts | 18 ------ src/routes/api/auth/index.ts | 19 ++++-- src/schemas/auth.ts | 4 +- test/plugins/repository.test.ts | 67 ++++++++++++++++++++ test/plugins/support.test.ts | 13 ---- 11 files changed, 266 insertions(+), 44 deletions(-) create mode 100644 scripts/seed-database.js create mode 100644 src/plugins/custom/repository.ts delete mode 100644 src/plugins/support.ts create mode 100644 test/plugins/repository.test.ts delete mode 100644 test/plugins/support.test.ts diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts index 4382db67..8ba7dd02 100644 --- a/@types/fastify/index.d.ts +++ b/@types/fastify/index.d.ts @@ -1,11 +1,12 @@ import { MySQLPromisePool } from "@fastify/mysql"; import { Static } from "@sinclair/typebox"; import { CredentialsSchema } from "../../src/schemas/auth.ts"; +import { IRepository } from '../../src/plugins/custom/repository.ts' declare module "fastify" { export interface FastifyInstance { - someSupport(): void; mysql: MySQLPromisePool; + repository: IRepository; config: { PORT: number; MYSQL_HOST: string; diff --git a/package.json b/package.json index f05bf662..6585329b 100644 --- a/package.json +++ b/package.json @@ -10,13 +10,14 @@ "scripts": { "build": "rm -rf dist && tsc", "watch": "npm run build -- --watch", - "test": "tap test/**/*", + "test": "npm run seed:db && tap --jobs=1 test/**/*", "start": "fastify start -l info dist/app.js", "dev": "fastify start -w -l info -P dist/app.js", "standalone": "node --env-file=.env dist/server.js", "lint": "eslint --ignore-pattern=dist", "lint:fix": "npm run lint -- --fix", - "migrate": "node --env-file=.env scripts/migrate.js" + "migrate": "node --env-file=.env scripts/migrate.js", + "seed:db": "node --env-file=.env scripts/seed-database.js" }, "keywords": [], "author": "Michelet Jean ", @@ -34,15 +35,18 @@ "@fastify/type-provider-typebox": "^4.0.0", "@fastify/under-pressure": "^8.3.0", "@sinclair/typebox": "^0.32.31", + "bcrypt": "^5.1.1", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", "fastify-plugin": "^4.0.0", "postgrator": "^7.2.0" }, "devDependencies": { + "@types/bcrypt": "^5.0.2", "@types/node": "^20.14.2", "eslint": "^9.4.0", "fastify-tsconfig": "^2.0.0", + "mysql2": "^3.10.1", "neostandard": "^0.7.0", "tap": "^19.2.2", "typescript": "^5.4.5" diff --git a/scripts/migrate.js b/scripts/migrate.js index 1eab2ca4..4d0f91b5 100644 --- a/scripts/migrate.js +++ b/scripts/migrate.js @@ -2,8 +2,8 @@ import mysql from 'mysql2/promise' import path from 'path' import Postgrator from 'postgrator' -async function doMigration () { - const connection = await mysql.createConnection({ +async function doMigration (con) { + const connection = con ?? await mysql.createConnection({ multipleStatements: true, host: process.env.MYSQL_HOST, port: process.env.MYSQL_PORT, diff --git a/scripts/seed-database.js b/scripts/seed-database.js new file mode 100644 index 00000000..aed99dc4 --- /dev/null +++ b/scripts/seed-database.js @@ -0,0 +1,60 @@ +import { createConnection } from 'mysql2/promise' +import bcrypt from 'bcrypt' + +async function seed () { + const connection = await createConnection({ + multipleStatements: true, + host: process.env.MYSQL_HOST, + port: Number(process.env.MYSQL_PORT), + database: process.env.MYSQL_DATABASE, + user: process.env.MYSQL_USER, + password: process.env.MYSQL_PASSWORD + }) + + try { + await truncateTables(connection) + await seedUsers(connection) + } catch (error) { + console.error('Error seeding database:', error) + } finally { + await connection.end() + } +} + +async function truncateTables (connection) { + const [tables] = await connection.query('SHOW TABLES') + + if (tables.length > 0) { + const tableNames = tables.map((row) => row[`Tables_in_${process.env.MYSQL_DATABASE}`]) + const truncateQueries = tableNames.map((tableName) => `TRUNCATE TABLE \`${tableName}\``).join('; ') + + // Disable foreign key checks + await connection.query('SET FOREIGN_KEY_CHECKS = 0') + try { + await connection.query(truncateQueries) + console.log('All tables have been truncated successfully.') + } finally { + // Re-enable foreign key checks + await connection.query('SET FOREIGN_KEY_CHECKS = 1') + } + } +} + +async function seedUsers (connection) { + const usernames = ['basic', 'moderator', 'admin'] + + for (const username of usernames) { + const hash = await bcrypt.hash('password', 10) + + const insertUserQuery = ` + INSERT INTO users (username, password) + VALUES (?, ?) + ` + + await connection.execute(insertUserQuery, [username, hash]) + } + + console.log('Users have been seeded successfully.') +} + +seed() diff --git a/src/app.ts b/src/app.ts index 2dc96ca4..c121623d 100644 --- a/src/app.ts +++ b/src/app.ts @@ -10,11 +10,18 @@ export default async function serviceApp( fastify: FastifyInstance, opts: FastifyPluginOptions ) { - // This loads all plugins defined in plugins + // This loads all external plugins defined in plugins/external + // those should be registred first as your custom plugins might depend on them + fastify.register(fastifyAutoload, { + dir: path.join(import.meta.dirname, "plugins/external"), + options: { ...opts } + }); + + // This loads all your custom plugins defined in plugins/custom // those should be support plugins that are reused // through your application fastify.register(fastifyAutoload, { - dir: path.join(import.meta.dirname, "plugins"), + dir: path.join(import.meta.dirname, "plugins/custom"), options: { ...opts } }); diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts new file mode 100644 index 00000000..0368d97f --- /dev/null +++ b/src/plugins/custom/repository.ts @@ -0,0 +1,103 @@ +import { MySQLPromisePool } from "@fastify/mysql"; +import { FastifyInstance } from "fastify"; +import fp from "fastify-plugin"; +import { RowDataPacket, ResultSetHeader } from "mysql2"; + +export type IRepository = MySQLPromisePool & ReturnType; + +type QuerySeparator = 'AND' | 'OR' | ','; + +function createRepository(fastify: FastifyInstance) { + const processAssignementRecord = (record: Record, separator: QuerySeparator) => { + const keys = Object.keys(record); + const values = Object.values(record); + const clause = keys.map((key) => `${key} = ?`).join(` ${separator} `); + + return [clause, values] as const; + }; + + type QueryOptions = { + select?: string; + where?: Record; + }; + + type WriteOptions = { + data: Record; + where?: Record; + }; + + const repository = { + ...fastify.mysql, + find: async (table: string, opts: QueryOptions): Promise => { + const { select = '*', where = {1:1} } = opts; + const [clause, values] = processAssignementRecord(where, 'AND'); + + const query = `SELECT ${select} FROM ${table} WHERE ${clause} LIMIT 1`; + const [rows] = await fastify.mysql.query(query, values); + if (rows.length < 1) { + return null; + } + + return rows[0] as T; + }, + + findMany: async (table: string, opts: QueryOptions): Promise => { + const { select = '*', where = {1:1} } = opts; + const [clause, values] = processAssignementRecord(where, 'AND'); + + const query = `SELECT ${select} FROM ${table} WHERE ${clause}`; + const [rows] = await fastify.mysql.query(query, values); + + return rows as T[]; + }, + + create: async (table: string, opts: WriteOptions): Promise => { + const { data } = opts; + const columns = Object.keys(data).join(', '); + const placeholders = Object.keys(data).map(() => '?').join(', '); + const values = Object.values(data); + + const query = `INSERT INTO ${table} (${columns}) VALUES (${placeholders})`; + const [result] = await fastify.mysql.query(query, values); + + return result.insertId; + }, + + update: async (table: string, opts: WriteOptions): Promise => { + const { data, where = {} } = opts; + const [dataClause, dataValues] = processAssignementRecord(data, ','); + const [whereClause, whereValues] = processAssignementRecord(where, 'AND'); + + const query = `UPDATE ${table} SET ${dataClause} WHERE ${whereClause}`; + const [result] = await fastify.mysql.query(query, [...dataValues, ...whereValues]); + + return result.affectedRows; + }, + + delete: async (table: string, where: Record): Promise => { + const [clause, values] = processAssignementRecord(where, 'AND'); + + const query = `DELETE FROM ${table} WHERE ${clause}`; + const [result] = await fastify.mysql.query(query, values); + + return result.affectedRows; + } + }; + + return repository; +} + +/** + * The use of fastify-plugin is required to be able + * to export the decorators to the outer scope + * + * @see https://github.com/fastify/fastify-plugin + */ +export default fp( + async function (fastify) { + fastify.decorate("repository", createRepository(fastify)); + // You should name your plugins if you want to avoid name collisions + // and/or to perform dependency checks. + }, + { name: "repository", dependencies: ['mysql'] } +); diff --git a/src/plugins/support.ts b/src/plugins/support.ts deleted file mode 100644 index 79d8a983..00000000 --- a/src/plugins/support.ts +++ /dev/null @@ -1,18 +0,0 @@ -import fp from "fastify-plugin"; - -/** - * The use of fastify-plugin is required to be able - * to export the decorators to the outer scope - * - * @see https://github.com/fastify/fastify-plugin - */ -export default fp( - async function (fastify) { - fastify.decorate("someSupport", function () { - return "hugs"; - }); - // You should name your plugins if you want to avoid name collisions - // and/or to perform dependency checks. - }, - { name: "support" } -); diff --git a/src/routes/api/auth/index.ts b/src/routes/api/auth/index.ts index 8276ed4c..7420553b 100644 --- a/src/routes/api/auth/index.ts +++ b/src/routes/api/auth/index.ts @@ -2,7 +2,8 @@ import { FastifyPluginAsyncTypebox, Type } from "@fastify/type-provider-typebox"; -import { CredentialsSchema } from "../../../schemas/auth.js"; +import bcrypt from "bcrypt"; +import { CredentialsSchema, IAuth } from "../../../schemas/auth.js"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { fastify.post( @@ -24,14 +25,22 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { async function (request, reply) { const { username, password } = request.body; - if (username === "basic" && password === "password") { - const token = fastify.jwt.sign({ username }); + const user = await fastify.repository.find('users', { + select: 'username, password', + where: { username } + }) - return { token }; + if (user) { + const isPasswordValid = await bcrypt.compare(password, user.password); + if (isPasswordValid) { + const token = fastify.jwt.sign({ username: user.username }); + + return { token }; + } } reply.status(401); - + return { message: "Invalid username or password." }; } ); diff --git a/src/schemas/auth.ts b/src/schemas/auth.ts index 9d67c5a9..968e87d0 100644 --- a/src/schemas/auth.ts +++ b/src/schemas/auth.ts @@ -1,6 +1,8 @@ -import { Type } from "@sinclair/typebox"; +import { Static, Type } from "@sinclair/typebox"; export const CredentialsSchema = Type.Object({ username: Type.String(), password: Type.String() }); + +export interface IAuth extends Static {} diff --git a/test/plugins/repository.test.ts b/test/plugins/repository.test.ts new file mode 100644 index 00000000..38dcbbce --- /dev/null +++ b/test/plugins/repository.test.ts @@ -0,0 +1,67 @@ +import { test } from "tap"; +import assert from "node:assert"; +import { execSync } from "child_process"; +import Fastify from "fastify"; +import repository from "../../src/plugins/custom/repository.js"; +import * as envPlugin from "../../src/plugins/external/1-env.js"; +import * as mysqlPlugin from "../../src/plugins/external/mysql.js"; +import { IAuth } from '../../src/schemas/auth.js'; + +test("repository works standalone", async (t) => { + const app = Fastify(); + + t.before(() => execSync('npm run seed:db')) + + t.after(() => { + app.close(); + // Run the seed script again to clean up after tests + execSync('npm run seed:db'); + }); + + app.register(envPlugin.default, envPlugin.autoConfig); + app.register(mysqlPlugin.default, mysqlPlugin.autoConfig); + app.register(repository); + + await app.ready(); + + // Test find method + const user = await app.repository.find('users', { select: 'username', where: { username: 'basic' } }); + assert.deepStrictEqual(user, { username: 'basic' }); + + const firstUser = await app.repository.find('users', { select: 'username' }); + assert.deepStrictEqual(firstUser, { username: 'basic' }); + + const nullUser = await app.repository.find('users', { select: 'username', where: { username: 'unknown' } }); + assert.equal(nullUser, null); + + // Test findMany method + const users = await app.repository.findMany('users', { select: 'username', where: { username: 'basic' } }); + assert.deepStrictEqual(users, [ + { username: 'basic' } + ]); + + // Test findMany method + const allUsers = await app.repository.findMany('users', { select: 'username' }); + assert.deepStrictEqual(allUsers, [ + { username: 'basic' }, + { username: 'moderator' }, + { username: 'admin' } + ]); + + // Test create method + const newUserId = await app.repository.create('users', { data: { username: 'new_user', password: 'new_password' } }); + const newUser = await app.repository.find('users', { select: 'username', where: { id: newUserId } }); + assert.deepStrictEqual(newUser, { username: 'new_user' }); + + // Test update method + const updateCount = await app.repository.update('users', { data: { password: 'updated_password' }, where: { username: 'new_user' } }); + assert.equal(updateCount, 1); + const updatedUser = await app.repository.find('users', { select: 'password', where: { username: 'new_user' } }); + assert.deepStrictEqual(updatedUser, { password: 'updated_password' }); + + // Test delete method + const deleteCount = await app.repository.delete('users', { username: 'new_user' }); + assert.equal(deleteCount, 1); + const deletedUser = await app.repository.find('users', { select: 'username', where: { username: 'new_user' } }); + assert.equal(deletedUser, null); +}); diff --git a/test/plugins/support.test.ts b/test/plugins/support.test.ts deleted file mode 100644 index 9addc9cf..00000000 --- a/test/plugins/support.test.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { test } from "tap"; -import assert from "node:assert"; - -import Fastify from "fastify"; -import Support from "../../src/plugins/support.js"; - -test("support works standalone", async () => { - const fastify = Fastify(); - fastify.register(Support); - - await fastify.ready(); - assert.equal(fastify.someSupport(), "hugs"); -}); From 6c24c39459b195b967b187fccd2448209f7d29b2 Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 22 Jun 2024 12:35:23 +0200 Subject: [PATCH 076/100] fix: remove unused param con of doMigration --- scripts/migrate.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/scripts/migrate.js b/scripts/migrate.js index 4d0f91b5..1eab2ca4 100644 --- a/scripts/migrate.js +++ b/scripts/migrate.js @@ -2,8 +2,8 @@ import mysql from 'mysql2/promise' import path from 'path' import Postgrator from 'postgrator' -async function doMigration (con) { - const connection = con ?? await mysql.createConnection({ +async function doMigration () { + const connection = await mysql.createConnection({ multipleStatements: true, host: process.env.MYSQL_HOST, port: process.env.MYSQL_PORT, From 81d5950eaf9843e42608456ac118229b8450035d Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 22 Jun 2024 13:00:17 +0200 Subject: [PATCH 077/100] refactor: no need for seed database at the beggining of a test --- test/plugins/repository.test.ts | 2 -- 1 file changed, 2 deletions(-) diff --git a/test/plugins/repository.test.ts b/test/plugins/repository.test.ts index 38dcbbce..c6edac3d 100644 --- a/test/plugins/repository.test.ts +++ b/test/plugins/repository.test.ts @@ -10,8 +10,6 @@ import { IAuth } from '../../src/schemas/auth.js'; test("repository works standalone", async (t) => { const app = Fastify(); - t.before(() => execSync('npm run seed:db')) - t.after(() => { app.close(); // Run the seed script again to clean up after tests From a84e0a388fde76aa0d242fc684934b959ffbb5f5 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 11:35:01 +0200 Subject: [PATCH 078/100] test: ignore seed:db catch blocks --- scripts/seed-database.js | 3 +++ 1 file changed, 3 insertions(+) diff --git a/scripts/seed-database.js b/scripts/seed-database.js index aed99dc4..6d168d55 100644 --- a/scripts/seed-database.js +++ b/scripts/seed-database.js @@ -14,9 +14,12 @@ async function seed () { try { await truncateTables(connection) await seedUsers(connection) + + /* c8 ignore start */ } catch (error) { console.error('Error seeding database:', error) } finally { + /* c8 ignore end */ await connection.end() } } From dfde7eeab9664bdd56c364f0d5f75280e77b9925 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 11:35:44 +0200 Subject: [PATCH 079/100] fix: typo --- .env.example | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 184e85a7..3c91cae1 100644 --- a/.env.example +++ b/.env.example @@ -3,11 +3,11 @@ NODE_ENV=production # Database -MYSQL_HOST: localhost -MYSQL_PORT: 3306 -MYSQL_DATABASE: test_db -MYSQL_USER: test_user -MYSQL_PASSWORD: test_password +MYSQL_HOST=localhost +MYSQL_PORT=3306 +MYSQL_DATABASE=test_db +MYSQL_USER=test_user +MYSQL_PASSWORD=test_password # Server FASTIFY_CLOSE_GRACE_DELAY=1000 From abe48097fe70a5b32ed5ed6da14ffb49f4893a26 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 11:37:17 +0200 Subject: [PATCH 080/100] refactor: move type declarations outside plugin declaration --- src/plugins/custom/repository.ts | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts index 0368d97f..c4aa158e 100644 --- a/src/plugins/custom/repository.ts +++ b/src/plugins/custom/repository.ts @@ -5,7 +5,17 @@ import { RowDataPacket, ResultSetHeader } from "mysql2"; export type IRepository = MySQLPromisePool & ReturnType; -type QuerySeparator = 'AND' | 'OR' | ','; +type QuerySeparator = 'AND' | ','; + +type QueryOptions = { + select?: string; + where?: Record; +}; + +type WriteOptions = { + data: Record; + where?: Record; +}; function createRepository(fastify: FastifyInstance) { const processAssignementRecord = (record: Record, separator: QuerySeparator) => { @@ -16,16 +26,6 @@ function createRepository(fastify: FastifyInstance) { return [clause, values] as const; }; - type QueryOptions = { - select?: string; - where?: Record; - }; - - type WriteOptions = { - data: Record; - where?: Record; - }; - const repository = { ...fastify.mysql, find: async (table: string, opts: QueryOptions): Promise => { From 044baeadf99c8dca94b6ad9633dbb8b99a1289b3 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 11:59:51 +0200 Subject: [PATCH 081/100] chore: generate dummy .env during CI for scripts using -env-file=.env flag --- .github/workflows/ci.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 5be190d1..b4c246e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,9 @@ jobs: JWT_SECRET=$(openssl rand -hex 32) echo "JWT_SECRET=$JWT_SECRET" >> $GITHUB_ENV + - name: Generate dummy .env for scripts using -env-file=.env flag + run: touch .env + - name: Test env: MYSQL_HOST: localhost From e1ee800e5f7c3d20b21a61dc4dda56603af5d6f6 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 12:00:49 +0200 Subject: [PATCH 082/100] chore: can't --ignore-scripts if using bcrypt during CI --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b4c246e9..dac5f760 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -45,7 +45,7 @@ jobs: node-version: ${{ matrix.node-version }} - name: Install dependencies - run: npm i --ignore-scripts + run: npm i - name: Lint Code run: npm run lint From 0366dac0f684dd75d5d07aa74f7af18f08a99ceb Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 12:04:53 +0200 Subject: [PATCH 083/100] refactor: use IAuth interface tp type user on request --- @types/fastify/index.d.ts | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts index 8ba7dd02..a122bc31 100644 --- a/@types/fastify/index.d.ts +++ b/@types/fastify/index.d.ts @@ -1,6 +1,5 @@ import { MySQLPromisePool } from "@fastify/mysql"; -import { Static } from "@sinclair/typebox"; -import { CredentialsSchema } from "../../src/schemas/auth.ts"; +import { IAuth } from "../../src/schemas/auth.ts"; import { IRepository } from '../../src/plugins/custom/repository.ts' declare module "fastify" { @@ -19,7 +18,7 @@ declare module "fastify" { } export interface FastifyRequest { - user: Static + user: IAuth } } From 8897a411526effb71c41141d7e104d1233acbe43 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 12:08:20 +0200 Subject: [PATCH 084/100] fix: add blank line to migration files --- migrations/001.undo.users.sql | 2 +- migrations/002.undo.tasks.sql | 2 +- migrations/003.undo.user_tasks.sql | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/migrations/001.undo.users.sql b/migrations/001.undo.users.sql index 365a2107..c99ddcdc 100644 --- a/migrations/001.undo.users.sql +++ b/migrations/001.undo.users.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS users; \ No newline at end of file +DROP TABLE IF EXISTS users; diff --git a/migrations/002.undo.tasks.sql b/migrations/002.undo.tasks.sql index ddcb09ad..2ff13806 100644 --- a/migrations/002.undo.tasks.sql +++ b/migrations/002.undo.tasks.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS tasks; \ No newline at end of file +DROP TABLE IF EXISTS tasks; diff --git a/migrations/003.undo.user_tasks.sql b/migrations/003.undo.user_tasks.sql index 85d66c69..bb7bc57c 100644 --- a/migrations/003.undo.user_tasks.sql +++ b/migrations/003.undo.user_tasks.sql @@ -1 +1 @@ -DROP TABLE IF EXISTS user_tasks; \ No newline at end of file +DROP TABLE IF EXISTS user_tasks; From ec8005f88221f0a60c8a159aed909a0090274469 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 12:13:05 +0200 Subject: [PATCH 085/100] fix: remove useless comments --- scripts/seed-database.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/scripts/seed-database.js b/scripts/seed-database.js index 6d168d55..1658fc51 100644 --- a/scripts/seed-database.js +++ b/scripts/seed-database.js @@ -31,13 +31,11 @@ async function truncateTables (connection) { const tableNames = tables.map((row) => row[`Tables_in_${process.env.MYSQL_DATABASE}`]) const truncateQueries = tableNames.map((tableName) => `TRUNCATE TABLE \`${tableName}\``).join('; ') - // Disable foreign key checks await connection.query('SET FOREIGN_KEY_CHECKS = 0') try { await connection.query(truncateQueries) console.log('All tables have been truncated successfully.') } finally { - // Re-enable foreign key checks await connection.query('SET FOREIGN_KEY_CHECKS = 1') } } From 3a6dfa5035316ee570d6824bc9574722f5116062 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 13:44:47 +0200 Subject: [PATCH 086/100] refactor: declare type decoration of fastify instance in plugin files --- @types/fastify/fastify.d.ts | 7 +++++++ @types/fastify/index.d.ts | 25 ------------------------- src/plugins/custom/repository.ts | 6 ++++++ src/plugins/external/1-env.ts | 14 ++++++++++++++ src/plugins/external/mysql.ts | 8 +++++++- src/routes/api/autohooks.ts | 1 + 6 files changed, 35 insertions(+), 26 deletions(-) create mode 100644 @types/fastify/fastify.d.ts delete mode 100644 @types/fastify/index.d.ts diff --git a/@types/fastify/fastify.d.ts b/@types/fastify/fastify.d.ts new file mode 100644 index 00000000..d273e094 --- /dev/null +++ b/@types/fastify/fastify.d.ts @@ -0,0 +1,7 @@ +import { IAuth } from "../../src/schemas/auth.ts"; + +declare module "fastify" { + export interface FastifyRequest { + user: IAuth + } + } \ No newline at end of file diff --git a/@types/fastify/index.d.ts b/@types/fastify/index.d.ts deleted file mode 100644 index a122bc31..00000000 --- a/@types/fastify/index.d.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { MySQLPromisePool } from "@fastify/mysql"; -import { IAuth } from "../../src/schemas/auth.ts"; -import { IRepository } from '../../src/plugins/custom/repository.ts' - -declare module "fastify" { - export interface FastifyInstance { - mysql: MySQLPromisePool; - repository: IRepository; - config: { - PORT: number; - MYSQL_HOST: string; - MYSQL_PORT: string; - MYSQL_USER: string; - MYSQL_PASSWORD: string; - MYSQL_DATABASE: string; - JWT_SECRET: string; - }; - } - - export interface FastifyRequest { - user: IAuth - } -} - -export {}; diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts index c4aa158e..30588f84 100644 --- a/src/plugins/custom/repository.ts +++ b/src/plugins/custom/repository.ts @@ -3,6 +3,12 @@ import { FastifyInstance } from "fastify"; import fp from "fastify-plugin"; import { RowDataPacket, ResultSetHeader } from "mysql2"; +declare module "fastify" { + export interface FastifyInstance { + repository: IRepository; + } +} + export type IRepository = MySQLPromisePool & ReturnType; type QuerySeparator = 'AND' | ','; diff --git a/src/plugins/external/1-env.ts b/src/plugins/external/1-env.ts index 22facc3f..21238809 100644 --- a/src/plugins/external/1-env.ts +++ b/src/plugins/external/1-env.ts @@ -1,5 +1,19 @@ import env from "@fastify/env"; +declare module "fastify" { + export interface FastifyInstance { + config: { + PORT: number; + MYSQL_HOST: string; + MYSQL_PORT: string; + MYSQL_USER: string; + MYSQL_PASSWORD: string; + MYSQL_DATABASE: string; + JWT_SECRET: string; + }; + } +} + const schema = { type: "object", required: [ diff --git a/src/plugins/external/mysql.ts b/src/plugins/external/mysql.ts index e1ff5fe9..73051bf8 100644 --- a/src/plugins/external/mysql.ts +++ b/src/plugins/external/mysql.ts @@ -1,7 +1,13 @@ import fp from "fastify-plugin"; -import fastifyMysql from "@fastify/mysql"; +import fastifyMysql, { MySQLPromisePool } from "@fastify/mysql"; import { FastifyInstance } from "fastify"; +declare module "fastify" { + export interface FastifyInstance { + mysql: MySQLPromisePool; + } +} + export const autoConfig = (fastify: FastifyInstance) => { return { promise: true, diff --git a/src/routes/api/autohooks.ts b/src/routes/api/autohooks.ts index 39951f42..3a554d48 100644 --- a/src/routes/api/autohooks.ts +++ b/src/routes/api/autohooks.ts @@ -1,5 +1,6 @@ import { FastifyInstance } from "fastify"; + export default async function (fastify: FastifyInstance) { fastify.addHook("onRequest", async (request) => { if (!request.url.startsWith("/api/auth/login")) { From 95a1b1d97535e79ecaabbdccf64dfa6e20370596 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 14:20:46 +0200 Subject: [PATCH 087/100] fix: add blank line --- @types/fastify/fastify.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/@types/fastify/fastify.d.ts b/@types/fastify/fastify.d.ts index d273e094..dbebd154 100644 --- a/@types/fastify/fastify.d.ts +++ b/@types/fastify/fastify.d.ts @@ -2,6 +2,6 @@ import { IAuth } from "../../src/schemas/auth.ts"; declare module "fastify" { export interface FastifyRequest { - user: IAuth + user: IAuth } - } \ No newline at end of file +} From 776086b5c5c7c877067a8488abafb2f2210b18c0 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 16:20:10 +0200 Subject: [PATCH 088/100] feat: create scrypt plugin --- @types/node/environment.d.ts | 7 +++- package.json | 2 -- scripts/seed-database.js | 4 +-- src/plugins/custom/scrypt.ts | 69 ++++++++++++++++++++++++++++++++++++ src/routes/api/auth/index.ts | 4 +-- test/plugins/scrypt.test.ts | 28 +++++++++++++++ 6 files changed, 106 insertions(+), 8 deletions(-) create mode 100644 src/plugins/custom/scrypt.ts create mode 100644 test/plugins/scrypt.test.ts diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index 5ee721bb..7105b751 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -4,8 +4,13 @@ declare global { PORT: number; LOG_LEVEL: string; FASTIFY_CLOSE_GRACE_DELAY: number; + MYSQL_HOST: string + MYSQL_PORT: number + MYSQL_DATABASE: string + MYSQL_USER: string + MYSQL_PASSWORD: string } } } -export {}; +export {}; \ No newline at end of file diff --git a/package.json b/package.json index 6585329b..eba29c6b 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,12 @@ "@fastify/type-provider-typebox": "^4.0.0", "@fastify/under-pressure": "^8.3.0", "@sinclair/typebox": "^0.32.31", - "bcrypt": "^5.1.1", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", "fastify-plugin": "^4.0.0", "postgrator": "^7.2.0" }, "devDependencies": { - "@types/bcrypt": "^5.0.2", "@types/node": "^20.14.2", "eslint": "^9.4.0", "fastify-tsconfig": "^2.0.0", diff --git a/scripts/seed-database.js b/scripts/seed-database.js index 1658fc51..d12de27b 100644 --- a/scripts/seed-database.js +++ b/scripts/seed-database.js @@ -1,5 +1,4 @@ import { createConnection } from 'mysql2/promise' -import bcrypt from 'bcrypt' async function seed () { const connection = await createConnection({ @@ -45,8 +44,7 @@ async function seedUsers (connection) { const usernames = ['basic', 'moderator', 'admin'] for (const username of usernames) { - const hash = await bcrypt.hash('password', 10) - + const hash = '918933f991bbf22eade96420811e46b4.b2e2105880b90b66bf6d6247a42a81368819a1c57c07165cf8b25df80b5752bb' const insertUserQuery = ` INSERT INTO users (username, password) VALUES (?, ?) diff --git a/src/plugins/custom/scrypt.ts b/src/plugins/custom/scrypt.ts new file mode 100644 index 00000000..d8f613f0 --- /dev/null +++ b/src/plugins/custom/scrypt.ts @@ -0,0 +1,69 @@ +import fp from 'fastify-plugin'; +import { scrypt, timingSafeEqual, randomBytes } from 'crypto' + +declare module "fastify" { + export interface FastifyInstance { + hash: typeof scryptHash; + compare: typeof compare + } + } + +const SCRYPT_KEYLEN = 32 +const SCRYPT_COST = 65536 +const SCRYPT_BLOCK_SIZE = 8 +const SCRYPT_PARALLELIZATION = 2 +const SCRYPT_MAXMEM = 128 * SCRYPT_COST * SCRYPT_BLOCK_SIZE * 2 + +// exported because used to seed the database +async function scryptHash(value: string): Promise { + return new Promise((resolve, reject) => { + const salt = randomBytes(Math.min(16, SCRYPT_KEYLEN / 2)) + + scrypt(value, salt, SCRYPT_KEYLEN, { + cost: SCRYPT_COST, + blockSize: SCRYPT_BLOCK_SIZE, + parallelization: SCRYPT_PARALLELIZATION, + maxmem: SCRYPT_MAXMEM + }, function (error, key) { + /* c8 ignore start - Requires extreme or impractical configuration values */ + if (error !== null) { + reject(error) + } + /* c8 ignore end */ + else { + resolve(`${salt.toString('hex')}.${key.toString('hex')}`) + } + }) + }) +} + +async function compare(value: string, hash: string): Promise { + const [salt, hashed] = hash.split('.') + const saltBuffer = Buffer.from(salt, 'hex'); + const hashedBuffer = Buffer.from(hashed, 'hex') + + return new Promise((resolve) => { + scrypt(value, saltBuffer, SCRYPT_KEYLEN, { + cost: SCRYPT_COST, + blockSize: SCRYPT_BLOCK_SIZE, + parallelization: SCRYPT_PARALLELIZATION, + maxmem: SCRYPT_MAXMEM + }, function (error, key) { + /* c8 ignore start - Requires extreme or impractical configuration values */ + if (error !== null) { + resolve(false) + } + /* c8 ignore end */ + else { + resolve(timingSafeEqual(key, hashedBuffer)) + } + }) + }) +} + +export default fp(async (fastify) => { + fastify.decorate('hash', scryptHash); + fastify.decorate('compare', compare); +}, { + name: 'scrypt' +}); diff --git a/src/routes/api/auth/index.ts b/src/routes/api/auth/index.ts index 7420553b..6dc8aed8 100644 --- a/src/routes/api/auth/index.ts +++ b/src/routes/api/auth/index.ts @@ -2,7 +2,6 @@ import { FastifyPluginAsyncTypebox, Type } from "@fastify/type-provider-typebox"; -import bcrypt from "bcrypt"; import { CredentialsSchema, IAuth } from "../../../schemas/auth.js"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { @@ -31,7 +30,8 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }) if (user) { - const isPasswordValid = await bcrypt.compare(password, user.password); + console.log(password, user.password) + const isPasswordValid = await fastify.compare(password, user.password); if (isPasswordValid) { const token = fastify.jwt.sign({ username: user.username }); diff --git a/test/plugins/scrypt.test.ts b/test/plugins/scrypt.test.ts new file mode 100644 index 00000000..5b8e6ac7 --- /dev/null +++ b/test/plugins/scrypt.test.ts @@ -0,0 +1,28 @@ +import { test } from "tap"; +import Fastify from "fastify"; +import scryptPlugin from "../../src/plugins/custom/scrypt.ts"; + +test("scrypt works standalone", async t => { + const app = Fastify(); + + t.teardown(() => app.close()); + + app.register(scryptPlugin); + + await app.ready(); + + const password = "test_password"; + const hash = await app.hash(password); + t.type(hash, 'string'); + + const isValid = await app.compare(password, hash); + t.ok(isValid, 'compare should return true for correct password'); + + const isInvalid = await app.compare("wrong_password", hash); + t.notOk(isInvalid, 'compare should return false for incorrect password'); + + await t.rejects( + () => app.compare(password, "malformed_hash"), + 'compare should throw an error for malformed hash' + ); +}); From 59d948472d1c6df27feab0b95ef032e5827a6af9 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 16:20:10 +0200 Subject: [PATCH 089/100] feat: create scrypt plugin --- @types/node/environment.d.ts | 2 +- package.json | 2 -- scripts/seed-database.js | 5 ++- src/plugins/custom/scrypt.ts | 69 ++++++++++++++++++++++++++++++++++++ src/routes/api/auth/index.ts | 3 +- test/plugins/scrypt.test.ts | 28 +++++++++++++++ 6 files changed, 101 insertions(+), 8 deletions(-) create mode 100644 src/plugins/custom/scrypt.ts create mode 100644 test/plugins/scrypt.test.ts diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index 5ee721bb..bc2017f7 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -8,4 +8,4 @@ declare global { } } -export {}; +export {}; \ No newline at end of file diff --git a/package.json b/package.json index 6585329b..eba29c6b 100644 --- a/package.json +++ b/package.json @@ -35,14 +35,12 @@ "@fastify/type-provider-typebox": "^4.0.0", "@fastify/under-pressure": "^8.3.0", "@sinclair/typebox": "^0.32.31", - "bcrypt": "^5.1.1", "fastify": "^4.26.1", "fastify-cli": "^6.1.1", "fastify-plugin": "^4.0.0", "postgrator": "^7.2.0" }, "devDependencies": { - "@types/bcrypt": "^5.0.2", "@types/node": "^20.14.2", "eslint": "^9.4.0", "fastify-tsconfig": "^2.0.0", diff --git a/scripts/seed-database.js b/scripts/seed-database.js index 1658fc51..1c5acc93 100644 --- a/scripts/seed-database.js +++ b/scripts/seed-database.js @@ -1,5 +1,4 @@ import { createConnection } from 'mysql2/promise' -import bcrypt from 'bcrypt' async function seed () { const connection = await createConnection({ @@ -45,8 +44,8 @@ async function seedUsers (connection) { const usernames = ['basic', 'moderator', 'admin'] for (const username of usernames) { - const hash = await bcrypt.hash('password', 10) - + // Generated hash for plain text 'password' + const hash = '918933f991bbf22eade96420811e46b4.b2e2105880b90b66bf6d6247a42a81368819a1c57c07165cf8b25df80b5752bb' const insertUserQuery = ` INSERT INTO users (username, password) VALUES (?, ?) diff --git a/src/plugins/custom/scrypt.ts b/src/plugins/custom/scrypt.ts new file mode 100644 index 00000000..d8f613f0 --- /dev/null +++ b/src/plugins/custom/scrypt.ts @@ -0,0 +1,69 @@ +import fp from 'fastify-plugin'; +import { scrypt, timingSafeEqual, randomBytes } from 'crypto' + +declare module "fastify" { + export interface FastifyInstance { + hash: typeof scryptHash; + compare: typeof compare + } + } + +const SCRYPT_KEYLEN = 32 +const SCRYPT_COST = 65536 +const SCRYPT_BLOCK_SIZE = 8 +const SCRYPT_PARALLELIZATION = 2 +const SCRYPT_MAXMEM = 128 * SCRYPT_COST * SCRYPT_BLOCK_SIZE * 2 + +// exported because used to seed the database +async function scryptHash(value: string): Promise { + return new Promise((resolve, reject) => { + const salt = randomBytes(Math.min(16, SCRYPT_KEYLEN / 2)) + + scrypt(value, salt, SCRYPT_KEYLEN, { + cost: SCRYPT_COST, + blockSize: SCRYPT_BLOCK_SIZE, + parallelization: SCRYPT_PARALLELIZATION, + maxmem: SCRYPT_MAXMEM + }, function (error, key) { + /* c8 ignore start - Requires extreme or impractical configuration values */ + if (error !== null) { + reject(error) + } + /* c8 ignore end */ + else { + resolve(`${salt.toString('hex')}.${key.toString('hex')}`) + } + }) + }) +} + +async function compare(value: string, hash: string): Promise { + const [salt, hashed] = hash.split('.') + const saltBuffer = Buffer.from(salt, 'hex'); + const hashedBuffer = Buffer.from(hashed, 'hex') + + return new Promise((resolve) => { + scrypt(value, saltBuffer, SCRYPT_KEYLEN, { + cost: SCRYPT_COST, + blockSize: SCRYPT_BLOCK_SIZE, + parallelization: SCRYPT_PARALLELIZATION, + maxmem: SCRYPT_MAXMEM + }, function (error, key) { + /* c8 ignore start - Requires extreme or impractical configuration values */ + if (error !== null) { + resolve(false) + } + /* c8 ignore end */ + else { + resolve(timingSafeEqual(key, hashedBuffer)) + } + }) + }) +} + +export default fp(async (fastify) => { + fastify.decorate('hash', scryptHash); + fastify.decorate('compare', compare); +}, { + name: 'scrypt' +}); diff --git a/src/routes/api/auth/index.ts b/src/routes/api/auth/index.ts index 7420553b..c05df8f7 100644 --- a/src/routes/api/auth/index.ts +++ b/src/routes/api/auth/index.ts @@ -2,7 +2,6 @@ import { FastifyPluginAsyncTypebox, Type } from "@fastify/type-provider-typebox"; -import bcrypt from "bcrypt"; import { CredentialsSchema, IAuth } from "../../../schemas/auth.js"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { @@ -31,7 +30,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { }) if (user) { - const isPasswordValid = await bcrypt.compare(password, user.password); + const isPasswordValid = await fastify.compare(password, user.password); if (isPasswordValid) { const token = fastify.jwt.sign({ username: user.username }); diff --git a/test/plugins/scrypt.test.ts b/test/plugins/scrypt.test.ts new file mode 100644 index 00000000..5b8e6ac7 --- /dev/null +++ b/test/plugins/scrypt.test.ts @@ -0,0 +1,28 @@ +import { test } from "tap"; +import Fastify from "fastify"; +import scryptPlugin from "../../src/plugins/custom/scrypt.ts"; + +test("scrypt works standalone", async t => { + const app = Fastify(); + + t.teardown(() => app.close()); + + app.register(scryptPlugin); + + await app.ready(); + + const password = "test_password"; + const hash = await app.hash(password); + t.type(hash, 'string'); + + const isValid = await app.compare(password, hash); + t.ok(isValid, 'compare should return true for correct password'); + + const isInvalid = await app.compare("wrong_password", hash); + t.notOk(isInvalid, 'compare should return false for incorrect password'); + + await t.rejects( + () => app.compare(password, "malformed_hash"), + 'compare should throw an error for malformed hash' + ); +}); From 6bce950a907939daca0277fd8fef7e345fc248a8 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 16:25:00 +0200 Subject: [PATCH 090/100] fix: add blank line --- @types/node/environment.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/@types/node/environment.d.ts b/@types/node/environment.d.ts index bc2017f7..5ee721bb 100644 --- a/@types/node/environment.d.ts +++ b/@types/node/environment.d.ts @@ -8,4 +8,4 @@ declare global { } } -export {}; \ No newline at end of file +export {}; From 5cd29c8537a77dbbc4b3ab39f338a5ed7684c4e5 Mon Sep 17 00:00:00 2001 From: jean Date: Tue, 25 Jun 2024 18:45:48 +0200 Subject: [PATCH 091/100] fix: revert timingSafeEqual in error branch --- src/plugins/custom/scrypt.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/plugins/custom/scrypt.ts b/src/plugins/custom/scrypt.ts index d8f613f0..c4406789 100644 --- a/src/plugins/custom/scrypt.ts +++ b/src/plugins/custom/scrypt.ts @@ -51,6 +51,7 @@ async function compare(value: string, hash: string): Promise { }, function (error, key) { /* c8 ignore start - Requires extreme or impractical configuration values */ if (error !== null) { + timingSafeEqual(hashedBuffer, hashedBuffer) resolve(false) } /* c8 ignore end */ From 4031f28cf45aeca66089df97b57ab6a1c933f3a2 Mon Sep 17 00:00:00 2001 From: Jean <110341611+jean-michelet@users.noreply.github.com> Date: Wed, 26 Jun 2024 12:20:27 +0200 Subject: [PATCH 092/100] Update src/plugins/custom/repository.ts Co-authored-by: KaKa <23028015+climba03003@users.noreply.github.com> Signed-off-by: Jean <110341611+jean-michelet@users.noreply.github.com> --- src/plugins/custom/repository.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts index 30588f84..db03a209 100644 --- a/src/plugins/custom/repository.ts +++ b/src/plugins/custom/repository.ts @@ -9,7 +9,7 @@ declare module "fastify" { } } -export type IRepository = MySQLPromisePool & ReturnType; +export type Repository = MySQLPromisePool & ReturnType; type QuerySeparator = 'AND' | ','; From 26acff69338bc25edf6bbeed0bfff1c566341ea8 Mon Sep 17 00:00:00 2001 From: jean Date: Wed, 26 Jun 2024 12:22:42 +0200 Subject: [PATCH 093/100] refactor: remove I prefix from ts interface --- @types/fastify/fastify.d.ts | 4 ++-- src/plugins/custom/repository.ts | 2 +- src/plugins/custom/scrypt.ts | 6 +++--- src/routes/api/auth/index.ts | 4 ++-- src/schemas/auth.ts | 2 +- test/plugins/repository.test.ts | 18 +++++++++--------- 6 files changed, 18 insertions(+), 18 deletions(-) diff --git a/@types/fastify/fastify.d.ts b/@types/fastify/fastify.d.ts index dbebd154..f85cbfd5 100644 --- a/@types/fastify/fastify.d.ts +++ b/@types/fastify/fastify.d.ts @@ -1,7 +1,7 @@ -import { IAuth } from "../../src/schemas/auth.ts"; +import { Auth } from "../../src/schemas/auth.ts"; declare module "fastify" { export interface FastifyRequest { - user: IAuth + user: Auth } } diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts index db03a209..f35970b7 100644 --- a/src/plugins/custom/repository.ts +++ b/src/plugins/custom/repository.ts @@ -5,7 +5,7 @@ import { RowDataPacket, ResultSetHeader } from "mysql2"; declare module "fastify" { export interface FastifyInstance { - repository: IRepository; + repository: Repository; } } diff --git a/src/plugins/custom/scrypt.ts b/src/plugins/custom/scrypt.ts index c4406789..ad8dcfa6 100644 --- a/src/plugins/custom/scrypt.ts +++ b/src/plugins/custom/scrypt.ts @@ -55,9 +55,9 @@ async function compare(value: string, hash: string): Promise { resolve(false) } /* c8 ignore end */ - else { - resolve(timingSafeEqual(key, hashedBuffer)) - } + else { + resolve(timingSafeEqual(key, hashedBuffer)) + } }) }) } diff --git a/src/routes/api/auth/index.ts b/src/routes/api/auth/index.ts index c05df8f7..174a9821 100644 --- a/src/routes/api/auth/index.ts +++ b/src/routes/api/auth/index.ts @@ -2,7 +2,7 @@ import { FastifyPluginAsyncTypebox, Type } from "@fastify/type-provider-typebox"; -import { CredentialsSchema, IAuth } from "../../../schemas/auth.js"; +import { CredentialsSchema, Auth } from "../../../schemas/auth.js"; const plugin: FastifyPluginAsyncTypebox = async (fastify) => { fastify.post( @@ -24,7 +24,7 @@ const plugin: FastifyPluginAsyncTypebox = async (fastify) => { async function (request, reply) { const { username, password } = request.body; - const user = await fastify.repository.find('users', { + const user = await fastify.repository.find('users', { select: 'username, password', where: { username } }) diff --git a/src/schemas/auth.ts b/src/schemas/auth.ts index 968e87d0..83ba0b0d 100644 --- a/src/schemas/auth.ts +++ b/src/schemas/auth.ts @@ -5,4 +5,4 @@ export const CredentialsSchema = Type.Object({ password: Type.String() }); -export interface IAuth extends Static {} +export interface Auth extends Static {} diff --git a/test/plugins/repository.test.ts b/test/plugins/repository.test.ts index c6edac3d..4bba1d39 100644 --- a/test/plugins/repository.test.ts +++ b/test/plugins/repository.test.ts @@ -5,7 +5,7 @@ import Fastify from "fastify"; import repository from "../../src/plugins/custom/repository.js"; import * as envPlugin from "../../src/plugins/external/1-env.js"; import * as mysqlPlugin from "../../src/plugins/external/mysql.js"; -import { IAuth } from '../../src/schemas/auth.js'; +import { Auth } from '../../src/schemas/auth.js'; test("repository works standalone", async (t) => { const app = Fastify(); @@ -23,23 +23,23 @@ test("repository works standalone", async (t) => { await app.ready(); // Test find method - const user = await app.repository.find('users', { select: 'username', where: { username: 'basic' } }); + const user = await app.repository.find('users', { select: 'username', where: { username: 'basic' } }); assert.deepStrictEqual(user, { username: 'basic' }); - const firstUser = await app.repository.find('users', { select: 'username' }); + const firstUser = await app.repository.find('users', { select: 'username' }); assert.deepStrictEqual(firstUser, { username: 'basic' }); - const nullUser = await app.repository.find('users', { select: 'username', where: { username: 'unknown' } }); + const nullUser = await app.repository.find('users', { select: 'username', where: { username: 'unknown' } }); assert.equal(nullUser, null); // Test findMany method - const users = await app.repository.findMany('users', { select: 'username', where: { username: 'basic' } }); + const users = await app.repository.findMany('users', { select: 'username', where: { username: 'basic' } }); assert.deepStrictEqual(users, [ { username: 'basic' } ]); // Test findMany method - const allUsers = await app.repository.findMany('users', { select: 'username' }); + const allUsers = await app.repository.findMany('users', { select: 'username' }); assert.deepStrictEqual(allUsers, [ { username: 'basic' }, { username: 'moderator' }, @@ -48,18 +48,18 @@ test("repository works standalone", async (t) => { // Test create method const newUserId = await app.repository.create('users', { data: { username: 'new_user', password: 'new_password' } }); - const newUser = await app.repository.find('users', { select: 'username', where: { id: newUserId } }); + const newUser = await app.repository.find('users', { select: 'username', where: { id: newUserId } }); assert.deepStrictEqual(newUser, { username: 'new_user' }); // Test update method const updateCount = await app.repository.update('users', { data: { password: 'updated_password' }, where: { username: 'new_user' } }); assert.equal(updateCount, 1); - const updatedUser = await app.repository.find('users', { select: 'password', where: { username: 'new_user' } }); + const updatedUser = await app.repository.find('users', { select: 'password', where: { username: 'new_user' } }); assert.deepStrictEqual(updatedUser, { password: 'updated_password' }); // Test delete method const deleteCount = await app.repository.delete('users', { username: 'new_user' }); assert.equal(deleteCount, 1); - const deletedUser = await app.repository.find('users', { select: 'username', where: { username: 'new_user' } }); + const deletedUser = await app.repository.find('users', { select: 'username', where: { username: 'new_user' } }); assert.equal(deletedUser, null); }); From ccbecd4d6255a8f0ab8b0866df2a9387827bc211 Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 29 Jun 2024 06:50:39 +0200 Subject: [PATCH 094/100] fix: identifier typo --- src/plugins/custom/repository.ts | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts index f35970b7..8d60b63b 100644 --- a/src/plugins/custom/repository.ts +++ b/src/plugins/custom/repository.ts @@ -24,7 +24,7 @@ type WriteOptions = { }; function createRepository(fastify: FastifyInstance) { - const processAssignementRecord = (record: Record, separator: QuerySeparator) => { + const processAssignmentRecord = (record: Record, separator: QuerySeparator) => { const keys = Object.keys(record); const values = Object.values(record); const clause = keys.map((key) => `${key} = ?`).join(` ${separator} `); @@ -36,7 +36,7 @@ function createRepository(fastify: FastifyInstance) { ...fastify.mysql, find: async (table: string, opts: QueryOptions): Promise => { const { select = '*', where = {1:1} } = opts; - const [clause, values] = processAssignementRecord(where, 'AND'); + const [clause, values] = processAssignmentRecord(where, 'AND'); const query = `SELECT ${select} FROM ${table} WHERE ${clause} LIMIT 1`; const [rows] = await fastify.mysql.query(query, values); @@ -49,7 +49,7 @@ function createRepository(fastify: FastifyInstance) { findMany: async (table: string, opts: QueryOptions): Promise => { const { select = '*', where = {1:1} } = opts; - const [clause, values] = processAssignementRecord(where, 'AND'); + const [clause, values] = processAssignmentRecord(where, 'AND'); const query = `SELECT ${select} FROM ${table} WHERE ${clause}`; const [rows] = await fastify.mysql.query(query, values); @@ -71,8 +71,8 @@ function createRepository(fastify: FastifyInstance) { update: async (table: string, opts: WriteOptions): Promise => { const { data, where = {} } = opts; - const [dataClause, dataValues] = processAssignementRecord(data, ','); - const [whereClause, whereValues] = processAssignementRecord(where, 'AND'); + const [dataClause, dataValues] = processAssignmentRecord(data, ','); + const [whereClause, whereValues] = processAssignmentRecord(where, 'AND'); const query = `UPDATE ${table} SET ${dataClause} WHERE ${whereClause}`; const [result] = await fastify.mysql.query(query, [...dataValues, ...whereValues]); @@ -81,7 +81,7 @@ function createRepository(fastify: FastifyInstance) { }, delete: async (table: string, where: Record): Promise => { - const [clause, values] = processAssignementRecord(where, 'AND'); + const [clause, values] = processAssignmentRecord(where, 'AND'); const query = `DELETE FROM ${table} WHERE ${clause}`; const [result] = await fastify.mysql.query(query, values); From 1e16652a262120189e88b6bb0ca67f2542d91281 Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 29 Jun 2024 06:52:38 +0200 Subject: [PATCH 095/100] fix: remove useless comment --- src/plugins/custom/scrypt.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/plugins/custom/scrypt.ts b/src/plugins/custom/scrypt.ts index ad8dcfa6..f643377b 100644 --- a/src/plugins/custom/scrypt.ts +++ b/src/plugins/custom/scrypt.ts @@ -14,7 +14,6 @@ const SCRYPT_BLOCK_SIZE = 8 const SCRYPT_PARALLELIZATION = 2 const SCRYPT_MAXMEM = 128 * SCRYPT_COST * SCRYPT_BLOCK_SIZE * 2 -// exported because used to seed the database async function scryptHash(value: string): Promise { return new Promise((resolve, reject) => { const salt = randomBytes(Math.min(16, SCRYPT_KEYLEN / 2)) From c6e7cd9a842b48e4f16eb73c77adf1a822d91330 Mon Sep 17 00:00:00 2001 From: jean Date: Sat, 29 Jun 2024 08:40:31 +0200 Subject: [PATCH 096/100] fix: run migration before test --- .github/workflows/ci.yml | 2 +- package.json | 6 +++--- test/plugins/repository.test.ts | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index dac5f760..66408af8 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -67,4 +67,4 @@ jobs: MYSQL_USER: test_user MYSQL_PASSWORD: test_password # JWT_SECRET is dynamically generated and loaded from the environment - run: npm run test + run: npm run db:migrate && npm run test diff --git a/package.json b/package.json index eba29c6b..bcabb572 100644 --- a/package.json +++ b/package.json @@ -10,14 +10,14 @@ "scripts": { "build": "rm -rf dist && tsc", "watch": "npm run build -- --watch", - "test": "npm run seed:db && tap --jobs=1 test/**/*", + "test": "npm run db:seed && tap --jobs=1 test/**/*", "start": "fastify start -l info dist/app.js", "dev": "fastify start -w -l info -P dist/app.js", "standalone": "node --env-file=.env dist/server.js", "lint": "eslint --ignore-pattern=dist", "lint:fix": "npm run lint -- --fix", - "migrate": "node --env-file=.env scripts/migrate.js", - "seed:db": "node --env-file=.env scripts/seed-database.js" + "db:migrate": "node --env-file=.env scripts/migrate.js", + "db:seed": "node --env-file=.env scripts/seed-database.js" }, "keywords": [], "author": "Michelet Jean ", diff --git a/test/plugins/repository.test.ts b/test/plugins/repository.test.ts index 4bba1d39..2f143a78 100644 --- a/test/plugins/repository.test.ts +++ b/test/plugins/repository.test.ts @@ -13,7 +13,7 @@ test("repository works standalone", async (t) => { t.after(() => { app.close(); // Run the seed script again to clean up after tests - execSync('npm run seed:db'); + execSync('npm run db:seed'); }); app.register(envPlugin.default, envPlugin.autoConfig); From 43136ed5d00966231ed02b7425714db00bf5a249 Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 14 Jul 2024 08:13:19 +0200 Subject: [PATCH 097/100] test: ensure access-control-allow-methods contains the expected methods --- src/plugins/external/cors.ts | 8 ++++---- test/app/cors.test.ts | 20 ++++++++++++++++++++ 2 files changed, 24 insertions(+), 4 deletions(-) create mode 100644 test/app/cors.test.ts diff --git a/src/plugins/external/cors.ts b/src/plugins/external/cors.ts index 26e9312d..1b6906af 100644 --- a/src/plugins/external/cors.ts +++ b/src/plugins/external/cors.ts @@ -1,12 +1,12 @@ -import cors from "@fastify/cors"; +import cors, { FastifyCorsOptions } from "@fastify/cors"; -export const autoConfig = { - // Set plugin options here +export const autoConfig: FastifyCorsOptions = { + methods: ['GET', 'POST', 'PUT', 'DELETE'] }; /** * This plugins enables the use of CORS. * - * @see https://github.com/fastify/fastify-cors + * @see {@link https://github.com/fastify/fastify-cors} */ export default cors; diff --git a/test/app/cors.test.ts b/test/app/cors.test.ts new file mode 100644 index 00000000..a4b56349 --- /dev/null +++ b/test/app/cors.test.ts @@ -0,0 +1,20 @@ +import { it } from "node:test"; +import { build } from "../helper.js"; +import assert from "node:assert"; + +it("should correctly handle CORS preflight for common methods and reject TRACE", async (t) => { + const app = await build(t); + + const res = await app.inject({ + method: "OPTIONS", + url: "/", + headers: { + "Origin": "http://example.com", + "Access-Control-Request-Method": "GET", + "Access-Control-Request-Headers": "Content-Type" + } + }); + + assert.strictEqual(res.statusCode, 204); + assert.strictEqual(res.headers['access-control-allow-methods'], 'GET, POST, PUT, DELETE'); +}); From 1af3f0a50f3dd30fd312da5492ccff40d7c0fb5f Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 14 Jul 2024 08:16:06 +0200 Subject: [PATCH 098/100] docs: add @link tag to urls --- .env.example | 2 +- src/plugins/custom/repository.ts | 2 +- src/plugins/external/1-env.ts | 2 +- src/plugins/external/helmet.ts | 2 +- src/plugins/external/sensible.ts | 2 +- src/plugins/external/swagger.ts | 4 ++-- src/plugins/external/under-pressure.ts | 4 ++-- src/server.ts | 2 +- 8 files changed, 10 insertions(+), 10 deletions(-) diff --git a/.env.example b/.env.example index 3c91cae1..f5e3749c 100644 --- a/.env.example +++ b/.env.example @@ -1,5 +1,5 @@ # Must always set to production -# @see https://www.youtube.com/watch?v=HMM7GJC5E2o +# @see {@link https://www.youtube.com/watch?v=HMM7GJC5E2o} NODE_ENV=production # Database diff --git a/src/plugins/custom/repository.ts b/src/plugins/custom/repository.ts index 8d60b63b..707a7c49 100644 --- a/src/plugins/custom/repository.ts +++ b/src/plugins/custom/repository.ts @@ -97,7 +97,7 @@ function createRepository(fastify: FastifyInstance) { * The use of fastify-plugin is required to be able * to export the decorators to the outer scope * - * @see https://github.com/fastify/fastify-plugin + * @see {@link https://github.com/fastify/fastify-plugin} */ export default fp( async function (fastify) { diff --git a/src/plugins/external/1-env.ts b/src/plugins/external/1-env.ts index 21238809..c14221de 100644 --- a/src/plugins/external/1-env.ts +++ b/src/plugins/external/1-env.ts @@ -75,6 +75,6 @@ export const autoConfig = { /** * This plugins helps to check environment variables. * - * @see https://github.com/fastify/fastify-env + * @see {@link https://github.com/fastify/fastify-env} */ export default env; diff --git a/src/plugins/external/helmet.ts b/src/plugins/external/helmet.ts index 9f7a3b6a..86f7e5e8 100644 --- a/src/plugins/external/helmet.ts +++ b/src/plugins/external/helmet.ts @@ -7,6 +7,6 @@ export const autoConfig = { /** * This plugins sets the basic security headers. * - * @see https://github.com/fastify/fastify-helmet + * @see {@link https://github.com/fastify/fastify-helmet} */ export default helmet; diff --git a/src/plugins/external/sensible.ts b/src/plugins/external/sensible.ts index 296e4d57..fba13930 100644 --- a/src/plugins/external/sensible.ts +++ b/src/plugins/external/sensible.ts @@ -7,6 +7,6 @@ export const autoConfig = { /** * This plugin adds some utilities to handle http errors * - * @see https://github.com/fastify/fastify-sensible + * @see {@link https://github.com/fastify/fastify-sensible} */ export default sensible; diff --git a/src/plugins/external/swagger.ts b/src/plugins/external/swagger.ts index f544d2f1..ede64459 100644 --- a/src/plugins/external/swagger.ts +++ b/src/plugins/external/swagger.ts @@ -6,7 +6,7 @@ export default fp(async function (fastify) { /** * A Fastify plugin for serving Swagger (OpenAPI v2) or OpenAPI v3 schemas * - * @see https://github.com/fastify/fastify-swagger + * @see {@link https://github.com/fastify/fastify-swagger} */ await fastify.register(fastifySwagger, { hideUntagged: true, @@ -22,7 +22,7 @@ export default fp(async function (fastify) { /** * A Fastify plugin for serving Swagger UI. * - * @see https://github.com/fastify/fastify-swagger-ui + * @see {@link https://github.com/fastify/fastify-swagger-ui} */ await fastify.register(fastifySwaggerUi, { // Set plugin options here diff --git a/src/plugins/external/under-pressure.ts b/src/plugins/external/under-pressure.ts index f8213fc0..e9efd064 100644 --- a/src/plugins/external/under-pressure.ts +++ b/src/plugins/external/under-pressure.ts @@ -33,10 +33,10 @@ export const autoConfig = (fastify: FastifyInstance) => { * A Fastify plugin for mesuring process load and automatically * handle of "Service Unavailable" * - * @see https://github.com/fastify/under-pressure + * @see {@link https://github.com/fastify/under-pressure} * * Video on the topic: Do not thrash the event loop - * @see https://www.youtube.com/watch?v=VI29mUA8n9w + * @see {@link https://www.youtube.com/watch?v=VI29mUA8n9w} */ export default fp(fastifyUnderPressure, { dependencies: ["mysql"] diff --git a/src/server.ts b/src/server.ts index e2b8673e..f45c8868 100644 --- a/src/server.ts +++ b/src/server.ts @@ -16,7 +16,7 @@ import serviceApp from "./app.js"; /** * Do not use NODE_ENV to determine what logger (or any env related feature) to use - * @see https://www.youtube.com/watch?v=HMM7GJC5E2o + * @see {@link https://www.youtube.com/watch?v=HMM7GJC5E2o} */ function getLoggerOptions() { // Only if the program is running in an interactive terminal From 8fb8618ad4c1985d1a31c3c944ba024761bc1950 Mon Sep 17 00:00:00 2001 From: Jean <110341611+jean-michelet@users.noreply.github.com> Date: Sun, 14 Jul 2024 08:19:01 +0200 Subject: [PATCH 099/100] Update src/app.ts Co-authored-by: Frazer Smith Signed-off-by: Jean <110341611+jean-michelet@users.noreply.github.com> --- src/app.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app.ts b/src/app.ts index c121623d..4e9b57ca 100644 --- a/src/app.ts +++ b/src/app.ts @@ -11,7 +11,7 @@ export default async function serviceApp( opts: FastifyPluginOptions ) { // This loads all external plugins defined in plugins/external - // those should be registred first as your custom plugins might depend on them + // those should be registered first as your custom plugins might depend on them fastify.register(fastifyAutoload, { dir: path.join(import.meta.dirname, "plugins/external"), options: { ...opts } From 6eb86ba3647b786c40b4b3802f91eca7bd3f19a1 Mon Sep 17 00:00:00 2001 From: jean Date: Sun, 14 Jul 2024 10:53:12 +0200 Subject: [PATCH 100/100] fix: test name --- test/app/cors.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/app/cors.test.ts b/test/app/cors.test.ts index a4b56349..9893ab81 100644 --- a/test/app/cors.test.ts +++ b/test/app/cors.test.ts @@ -2,7 +2,7 @@ import { it } from "node:test"; import { build } from "../helper.js"; import assert from "node:assert"; -it("should correctly handle CORS preflight for common methods and reject TRACE", async (t) => { +it("should correctly handle CORS preflight requests", async (t) => { const app = await build(t); const res = await app.inject({