Skip to content

Commit

Permalink
Migrate to MongoDB
Browse files Browse the repository at this point in the history
  • Loading branch information
textbook committed Sep 16, 2024
1 parent 3e35855 commit f55e396
Show file tree
Hide file tree
Showing 17 changed files with 546 additions and 320 deletions.
6 changes: 3 additions & 3 deletions api/app.js
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import express from "express";

import apiRouter from "./api.js";
import db from "./db.js";
import config from "./utils/config.cjs";
import { testConnection } from "./db.js";
import config from "./utils/config.js";
import {
asyncHandler,
clientRouter,
Expand All @@ -27,7 +27,7 @@ if (config.production) {
app.get(
"/healthz",
asyncHandler(async (_, res) => {
await db.query("SELECT 1;");
await testConnection();
res.sendStatus(200);
}),
);
Expand Down
35 changes: 16 additions & 19 deletions api/db.js
Original file line number Diff line number Diff line change
@@ -1,30 +1,27 @@
import pg from "pg";
import mongoose from "mongoose";

import logger from "./utils/logger.js";

/** @type {pg.Pool} */
let pool;

/**
* @param {import("pg").ClientConfig} config
* @param {string} uri
* @param {import("mongoose").ConnectOptions} [options]
*/
export const connectDb = async (config) => {
pool = new pg.Pool(config);
pool.on("error", (err) => logger.error("%O", err));
const client = await pool.connect();
logger.info("connected to %s", client.database);
client.release();
export const connectDb = async (uri, options) => {
mongoose.connection.on("error", (err) => logger.error("%O", err));
const client = await mongoose.connect(uri, {
bufferCommands: false,
...options,
});
logger.info("connected to %s", client.connection.name);
};

export const disconnectDb = async () => {
if (pool) {
await pool.end();
}
await mongoose.disconnect();
};

export default {
query(...args) {
logger.debug("Postgres query: %O", args);
return pool.query.apply(pool, args);
},
export const testConnection = async () => {
const state = mongoose.connection.readyState;
if (state !== mongoose.ConnectionStates.connected) {
throw new Error(`database connection state: ${mongoose.STATES[state]}`);
}
};
8 changes: 6 additions & 2 deletions api/messages/messageRepository.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import db from "../db.js";
import mongoose from "mongoose";

const MessageSchema = new mongoose.Schema({ content: String });

const Message = mongoose.model("messages", MessageSchema);

export async function getAll() {
const { rows } = await db.query("SELECT * FROM message;");
const rows = await Message.find();
return rows;
}
16 changes: 16 additions & 0 deletions api/migrate-mongo-config.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import config from "./utils/config.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

/** @type {import("migrate-mongo").config.Config} */
export default {
changelogCollectionName: "changelog",
migrationFileExtension: ".js",
migrationsDir: join(__dirname, "migrations"),
moduleSystem: "esm",
mongodb: { url: config.dbUrl },
useFileHash: false,
};
24 changes: 0 additions & 24 deletions api/migrations/1707922794590_welcome-message.cjs

This file was deleted.

21 changes: 21 additions & 0 deletions api/migrations/20240916162136-welcome-message.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
/**
* Template for
* {@link https://github.com/seppevs/migrate-mongo?tab=readme-ov-file#creating-a-new-migration-script defining migrations}.
*/

/**
* @param {import("mongodb").Db} db
* @param {import("mongodb").MongoClient} client
*/
export const up = async (db) => {
await db.collection("messages").insertOne({ content: "Hello, world!" });
};

/**
*
* @param {import("mongodb").Db} db
* @param {import("mongodb").MongoClient} client
*/
export const down = async (db) => {
await db.collection("messages").drop();
};
22 changes: 0 additions & 22 deletions api/migrations/config.cjs

This file was deleted.

20 changes: 20 additions & 0 deletions api/migrations/sample-migration.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,20 @@
/**
* Template for
* {@link https://github.com/seppevs/migrate-mongo?tab=readme-ov-file#creating-a-new-migration-script defining migrations}.
*/

/**
* @param {import("mongodb").Db} db
* @param {import("mongodb").MongoClient} client
*/
export const up = async (db, client) => {
// TODO write your migration here.
};

/**
* @param {import("mongodb").Db} db
* @param {import("mongodb").MongoClient} client
*/
export const down = async (db, client) => {
// TODO reverse your migration here.
};
17 changes: 0 additions & 17 deletions api/migrations/template.cjs

This file was deleted.

10 changes: 5 additions & 5 deletions api/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,21 +7,21 @@
"type": "module",
"scripts": {
"dev": "cross-env LOG_LEVEL=debug node --inspect --watch --watch-path . server.js",
"migration": "node-pg-migrate --config-file ./migrations/config.cjs",
"test": "cross-env DATABASE_URL=http://example.com NODE_OPTIONS='--experimental-vm-modules' jest",
"migration": "migrate-mongo",
"test": "cross-env MONGO_URL=http://example.com NODE_OPTIONS='--experimental-vm-modules' jest",
"test:watch": "npm run test -- --watch"
},
"dependencies": {
"dotenv": "^16.4.5",
"express": "^4.21.0",
"helmet": "^7.1.0",
"migrate-mongo": "^11.0.0",
"mongoose": "^8.6.2",
"morgan": "^1.10.0",
"node-pg-migrate": "^7.6.1",
"pg": "^8.12.0",
"winston": "^3.14.2"
},
"devDependencies": {
"@testcontainers/postgresql": "^10.13.1",
"@testcontainers/mongodb": "^10.13.1",
"eslint-plugin-jest": "^28.8.3",
"eslint-plugin-n": "^17.10.2",
"jest": "^29.7.0",
Expand Down
4 changes: 2 additions & 2 deletions api/server.js
Original file line number Diff line number Diff line change
@@ -1,10 +1,10 @@
import app from "./app.js";
import { connectDb } from "./db.js";
import config from "./utils/config.cjs";
import config from "./utils/config.js";
import logger from "./utils/logger.js";

const { port } = config;

await connectDb(config.dbConfig);
await connectDb(config.dbUrl);

app.listen(port, () => logger.info(`listening on ${port}`));
31 changes: 19 additions & 12 deletions api/setupTests.js
Original file line number Diff line number Diff line change
@@ -1,21 +1,22 @@
import { dirname, join } from "node:path";
import { fileURLToPath } from "node:url";

import { PostgreSqlContainer } from "@testcontainers/postgresql";
import { runner } from "node-pg-migrate";
import { MongoDBContainer } from "@testcontainers/mongodb";
import { config, database, up } from "migrate-mongo";

import { connectDb, disconnectDb } from "./db.js";
import logger from "./utils/logger.js";

const __dirname = dirname(fileURLToPath(import.meta.url));

/** @type {import("@testcontainers/postgresql").StartedPostgreSqlContainer} */
/** @type {import("@testcontainers/mongodb").StartedMongoDBContainer} */
let dbContainer;

beforeAll(async () => {
dbContainer = await new PostgreSqlContainer().start();
const connectionString = dbContainer.getConnectionUri();
dbContainer = await new MongoDBContainer().start();
const connectionString = dbContainer.getConnectionString();
await applyMigrations(connectionString);
await connectDb({ connectionString });
await connectDb(connectionString, { directConnection: true });
}, 60_000);

afterAll(async () => {
Expand All @@ -25,11 +26,17 @@ afterAll(async () => {
}
});

async function applyMigrations(databaseUrl) {
await runner({
databaseUrl,
dir: join(__dirname, "migrations"),
direction: "up",
ignorePattern: "(config|template)\\.cjs$",
async function applyMigrations(url) {
config.set({
changelogCollectionName: "changelog",
migrationFileExtension: ".js",
migrationsDir: join(__dirname, "migrations"),
mongodb: { url, options: { directConnection: true } },
useFileHash: false,
});
const { db, client } = await database.connect();
const migrations = await up(db, client);
for (const migration of migrations) {
logger.info("Run migration: %s", migration);
}
}
4 changes: 1 addition & 3 deletions api/utils/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,14 @@
- Import from `logger.js`; and
- Ignore `middleware.js` entirely (all predefined middleware is already used in `app.js`).

## `config.cjs`
## `config.js`

Creates and exposes an object representing the app's configuration. This centralises access to the environment and definition of default values.

[Dotenv] is used to load any configuration required from a `.env` file in the root of the repository.

To check if this is being used correctly, if you search your codebase, _all_ uses of `process.env` in `api/` should be in this file.

**Note** this is CommonJS (`.cjs`) rather than an ES module so that it can be used by `node-pg-migrate` and similar tools, to avoid duplicating configuration.

## `logger.js`

Creates, configures and exports a [Winston] logger, which you can then import and use elsewhere:
Expand Down
32 changes: 9 additions & 23 deletions api/utils/config.cjs → api/utils/config.js
Original file line number Diff line number Diff line change
@@ -1,41 +1,27 @@
/* eslint-disable no-restricted-syntax */
const { join, resolve } = require("node:path");
import { dirname, join, resolve } from "node:path";
import { fileURLToPath } from "node:url";

const { configDotenv } = require("dotenv");
import { configDotenv } from "dotenv";

const __dirname = dirname(fileURLToPath(import.meta.url));

const dotenvPath = resolve(
join(__dirname, "..", "..", process.env.DOTENV_CONFIG_PATH ?? ".env"),
);

configDotenv({ path: dotenvPath });

requireArgs(["DATABASE_URL"]);

const databaseUrl = new URL(process.env.DATABASE_URL);

const localDb = [
"0.0.0.0",
"127.0.0.1",
"localhost",
"host.docker.internal",
].includes(databaseUrl.hostname);
const sslMode = ["prefer", "require", "verify-ca", "verify-full"].includes(
databaseUrl.searchParams.get("sslmode") ?? process.env.PGSSLMODE,
);
requireArgs(["MONGO_URL"]);

/**
* @property {import("pg").ClientConfig} dbConfig
* @property {string} dbUrl
* @property {string} dotenvPath
* @property {string} logLevel
* @property {number} port
* @property {boolean} production
*/
module.exports = {
dbConfig: {
connectionString: databaseUrl.toString(),
connectionTimeoutMillis: 5_000,
ssl: localDb ? false : { rejectUnauthorized: sslMode },
},
export default {
dbUrl: process.env.MONGO_URL,
dotenvPath,
logLevel: process.env.LOG_LEVEL?.toLowerCase() ?? "info",
port: parseInt(process.env.PORT ?? "3000", 10),
Expand Down
2 changes: 1 addition & 1 deletion api/utils/logger.js
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import { createLogger, format, transports } from "winston";

import config from "./config.cjs";
import config from "./config.js";

const logger = createLogger({
format: format.combine(
Expand Down
5 changes: 3 additions & 2 deletions eslint.config.js
Original file line number Diff line number Diff line change
Expand Up @@ -44,15 +44,16 @@ export default [
},
],
},
ignores: ["api/utils/config.js"],
},
{
files: ["**/*.cjs"],
...nodePlugin.configs["flat/recommended-script"],
},
{
files: ["api/migrations/template.cjs"],
files: ["api/migrations/sample-migration.js"],
rules: {
"no-unused-vars": ["error", { argsIgnorePattern: "pgm" }],
"no-unused-vars": ["error", { argsIgnorePattern: "client|db" }],
},
},
{
Expand Down
Loading

0 comments on commit f55e396

Please sign in to comment.