Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 5 additions & 5 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,11 @@ CAN_DROP_DATABASE=0
CAN_SEED_DATABASE=0

# Database
MYSQL_HOST=localhost
MYSQL_PORT=3306
MYSQL_DATABASE=test_db
MYSQL_USER=test_user
MYSQL_PASSWORD=test_password
POSTGRES_HOST=localhost
POSTGRES_PORT=5432
POSTGRES_DATABASE=test_db
POSTGRES_USER=test_user
POSTGRES_PASSWORD=test_password

# Server
FASTIFY_CLOSE_GRACE_DELAY=1000
Expand Down
27 changes: 13 additions & 14 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -33,20 +33,19 @@ jobs:
node-version: [22, 24]

services:
mysql:
image: mysql:8.4
postgres:
image: postgres:18
ports:
- 3306:3306
- 5432:5432
env:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: test_db
MYSQL_USER: test_user
MYSQL_PASSWORD: test_password
POSTGRES_DB: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
options: >-
--health-cmd="mysqladmin ping -u$MYSQL_USER -p$MYSQL_PASSWORD"
--health-cmd="pg_isready -U $POSTGRES_USER -d $POSTGRES_DB"
--health-interval=10s
--health-timeout=5s
--health-retries=3
--health-retries=5

steps:
- uses: actions/checkout@v6
Expand Down Expand Up @@ -77,11 +76,11 @@ jobs:

- name: Test
env:
MYSQL_HOST: localhost
MYSQL_PORT: 3306
MYSQL_DATABASE: test_db
MYSQL_USER: test_user
MYSQL_PASSWORD: test_password
POSTGRES_HOST: localhost
POSTGRES_PORT: 5432
POSTGRES_DATABASE: test_db
POSTGRES_USER: test_user
POSTGRES_PASSWORD: test_password
# COOKIE_SECRET is dynamically generated and loaded from the environment
COOKIE_NAME: 'sessid'
RATE_LIMIT_MAX: 4
Expand Down
10 changes: 5 additions & 5 deletions @types/node/environment.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,11 +4,11 @@ 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
POSTGRES_HOST: string
POSTGRES_PORT: number
POSTGRES_DATABASE: string
POSTGRES_USER: string
POSTGRES_PASSWORD: string
}
}
}
Expand Down
3 changes: 1 addition & 2 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,8 +25,7 @@ Create a `.env` file based on `.env.example` and update values as needed.
Make sure `COOKIE_SECRET` is set to a secret with at least 32 characters.

### Database

You can run a MySQL instance with Docker:
You can run a PostgreSQL instance with Docker:
```bash
docker compose up
```
Expand Down
15 changes: 7 additions & 8 deletions docker-compose.yml
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
services:
db:
image: mysql:8.4
image: postgres:18
environment:
MYSQL_ROOT_PASSWORD: root_password
MYSQL_DATABASE: ${MYSQL_DATABASE}
MYSQL_USER: ${MYSQL_USER}
MYSQL_PASSWORD: ${MYSQL_PASSWORD}
POSTGRES_DB: ${POSTGRES_DATABASE}
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
ports:
- 3306:3306
- 5432:5432
healthcheck:
test: ["CMD", "mysqladmin", "ping", "-u${MYSQL_USER}", "-p${MYSQL_PASSWORD}"]
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d ${POSTGRES_DATABASE}"]
interval: 10s
timeout: 5s
retries: 3
volumes:
- db_data:/var/lib/mysql
- db_data:/var/lib/postgresql

volumes:
db_data:
6 changes: 3 additions & 3 deletions migrations/001.do.users.sql
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
CREATE TABLE users (
id INT AUTO_INCREMENT PRIMARY KEY,
id SERIAL PRIMARY KEY,
email VARCHAR(255) UNIQUE NOT NULL,
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
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP
);
6 changes: 3 additions & 3 deletions migrations/002.do.tasks.sql
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
CREATE TABLE tasks (
id INT AUTO_INCREMENT PRIMARY KEY,
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL,
author_id INT NOT NULL,
assigned_user_id INT,
filename VARCHAR(255),
status VARCHAR(50) NOT NULL,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
created_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMPTZ DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (author_id) REFERENCES users(id),
FOREIGN KEY (assigned_user_id) REFERENCES users(id)
);
2 changes: 1 addition & 1 deletion migrations/004.do.roles.sql
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
CREATE TABLE roles (
id INT AUTO_INCREMENT PRIMARY KEY,
id SERIAL PRIMARY KEY,
name VARCHAR(255) NOT NULL
);
2 changes: 1 addition & 1 deletion migrations/005.do.user_roles.sql
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
CREATE TABLE user_roles (
id INT AUTO_INCREMENT PRIMARY KEY,
id SERIAL PRIMARY KEY,
user_id INT NOT NULL,
role_id INT NOT NULL,
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
Expand Down
3 changes: 2 additions & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -54,7 +54,8 @@
"fastify": "^5.6.0",
"fastify-plugin": "^5.0.1",
"knex": "^3.1.0",
"mysql2": "^3.15.0",
"pg": "^8.16.3",
"pg-query-stream": "^4.10.3",

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this used anywhere?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I hit this bug in CI while running the test “Task image upload, retrieval and delete”:

Cannot find module 'pg-query-stream'
Run: https://github.com/fastify/demo/actions/runs/20621476554/job/59224136187

Adding the dependency fixes the error. What’s odd is that the tests pass locally without it.

Any idea why CI behaves differently here and how can I fix it?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is required by knex.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yess, it's required by knex as mentioned here.

"postgrator": "^8.0.0",
"sanitize-filename": "^1.6.3"
},
Expand Down
40 changes: 29 additions & 11 deletions scripts/create-database.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,48 @@
import { createConnection, Connection } from 'mysql2/promise'
import { Client } from 'pg'

if (Number(process.env.CAN_CREATE_DATABASE) !== 1) {
throw new Error("You can't create the database. Set `CAN_CREATE_DATABASE=1` environment variable to allow this operation.")
}

async function createDatabase () {
const connection = await createConnection({
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD
const databaseName = process.env.POSTGRES_DATABASE
if (!databaseName) {
throw new Error('Missing `POSTGRES_DATABASE` environment variable.')
}

const connection = new Client({
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT),
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: 'postgres'
})

try {
await createDB(connection)
console.log(`Database ${process.env.MYSQL_DATABASE} has been created successfully.`)
await connection.connect()
await createDB(connection, databaseName)
console.log(`Database ${databaseName} has been created successfully.`)
} catch (error) {
console.error('Error creating database:', error)
} finally {
await connection.end()
}
}

async function createDB (connection: Connection) {
await connection.query(`CREATE DATABASE IF NOT EXISTS \`${process.env.MYSQL_DATABASE}\``)
console.log(`Database ${process.env.MYSQL_DATABASE} created or already exists.`)
async function createDB (connection: Client, databaseName: string) {
const exists = await connection.query(
'SELECT 1 FROM pg_database WHERE datname = $1',
[databaseName]
)

if (exists.rowCount > 0) {
console.log(`Database ${databaseName} already exists.`)
return
}

const safeDbName = databaseName.replace(/"/g, '""')
await connection.query(`CREATE DATABASE "${safeDbName}"`)
console.log(`Database ${databaseName} created.`)
}

createDatabase()
35 changes: 24 additions & 11 deletions scripts/drop-database.ts
Original file line number Diff line number Diff line change
@@ -1,30 +1,43 @@
import { createConnection, Connection } from 'mysql2/promise'
import { Client } from 'pg'

if (Number(process.env.CAN_DROP_DATABASE) !== 1) {
throw new Error("You can't drop the database. Set `CAN_DROP_DATABASE=1` environment variable to allow this operation.")
}

async function dropDatabase () {
const connection = await createConnection({
host: process.env.MYSQL_HOST,
port: Number(process.env.MYSQL_PORT),
user: process.env.MYSQL_USER,
password: process.env.MYSQL_PASSWORD
const databaseName = process.env.POSTGRES_DATABASE
if (!databaseName) {
throw new Error('Missing `POSTGRES_DATABASE` environment variable.')
}

const connection = new Client({
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT),
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD,
database: 'postgres'
})

try {
await dropDB(connection)
console.log(`Database ${process.env.MYSQL_DATABASE} has been dropped successfully.`)
await connection.connect()
await dropDB(connection, databaseName)
console.log(`Database ${databaseName} has been dropped successfully.`)
} catch (error) {
console.error('Error dropping database:', error)
} finally {
await connection.end()
}
}

async function dropDB (connection: Connection) {
await connection.query(`DROP DATABASE IF EXISTS \`${process.env.MYSQL_DATABASE}\``)
console.log(`Database ${process.env.MYSQL_DATABASE} dropped.`)
async function dropDB (connection: Client, databaseName: string) {
await connection.query(
'SELECT pg_terminate_backend(pid) FROM pg_stat_activity WHERE datname = $1 AND pid <> pg_backend_pid()',
[databaseName]
)

const safeDbName = databaseName.replace(/"/g, '""')
await connection.query(`DROP DATABASE IF EXISTS "${safeDbName}"`)
console.log(`Database ${databaseName} dropped.`)
}

dropDatabase()
28 changes: 12 additions & 16 deletions scripts/migrate.ts
Original file line number Diff line number Diff line change
@@ -1,24 +1,21 @@
import mysql, { FieldPacket } from 'mysql2/promise'
import { Client, QueryResult } from 'pg'
import path from 'node:path'
import fs from 'node:fs'
import Postgrator from 'postgrator'

interface PostgratorResult {
rows: any;
fields: FieldPacket[];
}
type PostgratorResult = QueryResult

async function doMigration (): Promise<void> {
const connection = await mysql.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
const connection = new Client({
host: process.env.POSTGRES_HOST,
port: Number(process.env.POSTGRES_PORT),
database: process.env.POSTGRES_DATABASE,
user: process.env.POSTGRES_USER,
password: process.env.POSTGRES_PASSWORD
})

try {
await connection.connect()
const migrationDir = path.join(import.meta.dirname, '../migrations')

if (!fs.existsSync(migrationDir)) {
Expand All @@ -29,11 +26,10 @@ async function doMigration (): Promise<void> {

const postgrator = new Postgrator({
migrationPattern: path.join(migrationDir, '*'),
driver: 'mysql',
database: process.env.MYSQL_DATABASE,
driver: 'pg',
database: process.env.POSTGRES_DATABASE,
execQuery: async (query: string): Promise<PostgratorResult> => {
const [rows, fields] = await connection.query(query)
return { rows, fields }
return await connection.query(query)
},
schemaTable: 'schemaversion'
})
Expand Down
Loading