Skip to content

Commit

Permalink
db: add postgresql + drizzle support
Browse files Browse the repository at this point in the history
  • Loading branch information
espimarisa committed Aug 29, 2024
1 parent 1822783 commit 1c8a606
Show file tree
Hide file tree
Showing 16 changed files with 339 additions and 6 deletions.
24 changes: 24 additions & 0 deletions .env.example
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,27 @@ API_IPINFOIO_KEY =
# A Google Maps API key. Must have access to the Static Maps API. Remember to restrict your key!
# https://developers.google.com/maps/get-started
API_GOOGLEMAPS_KEY =

# The PostgreSQL user to authenticate to
# If you're using Docker with a default configuration, set this to "postgres"
POSTGRES_USER = "postgres"

# The PostgreSQL password to authenticate with
# If you're using Docker with a default configuration, set this to "postgres"
POSTGRES_PASSWORD = "postgres"

# The PostgreSQL host to connect to
# If you're using Docker, set this to "postgres" (or the DB container name)
POSTGRES_HOST = "127.0.0.1"

# The port to connect to PostgreSQL on
# By default, this port is 5432
POSTGRES_PORT = 5432

# The database to use and/or create
# We create a "hibiki" database by default
POSTGRES_DB = "hibiki"

# The database schema to use and/or create
# We use a "hibiki" schema by default
POSTGRES_SCHEMA = "hibiki"
Binary file modified bun.lockb
Binary file not shown.
19 changes: 18 additions & 1 deletion docker-compose.yml
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,24 @@ services:
context: .
dockerfile: Dockerfile
container_name: hibiki
depends_on:
- postgres
restart: unless-stopped

postgres:
image: postgres:latest
container_name: postgres
hostname: ${POSTGRES_HOST}
environment:
POSTGRES_USER: ${POSTGRES_USER}
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
POSTGRES_DB: ${POSTGRES_DB}
ports:
- ${POSTGRES_PORT}:${POSTGRES_PORT}
volumes:
- postgres-data:/var/lib/postgresql/data
restart: unless-stopped

volumes:
hibiki-data:
postgres-data:
hibiki:
18 changes: 18 additions & 0 deletions drizzle.config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
import { env } from "$utils/env.ts";
import { type Config, defineConfig } from "drizzle-kit";

// biome-ignore lint/style/noDefaultExport: Config file
export default defineConfig({
out: "./drizzle",
schema: "./src/db/schema/*",
dialect: "postgresql",
dbCredentials: {
host: env.POSTGRES_HOST,
port: Number.parseInt(env.POSTGRES_PORT),
user: env.POSTGRES_USER,
password: env.POSTGRES_PASSWORD,
database: env.POSTGRES_DB,
},
verbose: true,
strict: true,
}) satisfies Config;
9 changes: 9 additions & 0 deletions drizzle/0000_cold_grey_gargoyle.sql
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
CREATE TABLE IF NOT EXISTS "guild_config" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"guild_id" text NOT NULL
);
--> statement-breakpoint
CREATE TABLE IF NOT EXISTS "user_config" (
"id" uuid PRIMARY KEY DEFAULT gen_random_uuid() NOT NULL,
"user_id" text NOT NULL
);
62 changes: 62 additions & 0 deletions drizzle/meta/0000_snapshot.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
{
"id": "3e0e4620-044b-4dc8-afa3-6628db6c803b",
"prevId": "00000000-0000-0000-0000-000000000000",
"version": "7",
"dialect": "postgresql",
"tables": {
"public.guild_config": {
"name": "guild_config",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"guild_id": {
"name": "guild_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
},
"public.user_config": {
"name": "user_config",
"schema": "",
"columns": {
"id": {
"name": "id",
"type": "uuid",
"primaryKey": true,
"notNull": true,
"default": "gen_random_uuid()"
},
"user_id": {
"name": "user_id",
"type": "text",
"primaryKey": false,
"notNull": true
}
},
"indexes": {},
"foreignKeys": {},
"compositePrimaryKeys": {},
"uniqueConstraints": {}
}
},
"enums": {},
"schemas": {},
"sequences": {},
"_meta": {
"columns": {},
"schemas": {},
"tables": {}
}
}
13 changes: 13 additions & 0 deletions drizzle/meta/_journal.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"version": "7",
"dialect": "postgresql",
"entries": [
{
"idx": 0,
"version": "7",
"when": 1724922837098,
"tag": "0000_cold_grey_gargoyle",
"breakpoints": true
}
]
}
14 changes: 11 additions & 3 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,15 @@
"packageManager": "[email protected]",
"scripts": {
"check": "tsc --noEmit",
"db:generate": "drizzle-kit generate",
"db:migrate": "drizzle-kit migrate",
"db:push": "drizzle-kit push",
"db:studio": "bun run db:generate && drizzle-kit studio",
"deps:update": "bunx npm-check-updates --target latest --root --upgrade",
"dev": "NODE_ENV=development bun run --hot ./src/index.ts",
"dev": "NODE_ENV=development bun run db:generate && bun run db:push && bun run --hot ./src/index.ts",
"lint": "biome check .",
"lint:fix": "biome check --apply .",
"start": "NODE_ENV=production bun run ./src/index.ts",
"start": "NODE_ENV=production bun run db:generate && bun run ./src/index.ts",
"test": "bun run check && bun run lint",
"format": "biome format --write ."
},
Expand All @@ -24,16 +28,20 @@
"date-fns": "^3.6.0",
"discord-api-types": "^0.37.98",
"discord.js": "^14.15.3",
"drizzle-orm": "^0.33.0",
"i18next": "^23.14.0",
"i18next-fs-backend": "^2.3.2",
"pino": "^9.3.2",
"pino-pretty": "^11.2.2",
"postgres": "^3.4.4",
"tslib": "^2.7.0",
"zod": "^3.23.8"
},
"devDependencies": {
"@biomejs/biome": "^1.8.3",
"bun-types": "^1.1.26",
"drizzle-kit": "^0.24.2",
"typescript": "^5.5.4"
}
},
"trustedDependencies": ["@biomejs/biome", "esbuild"]
}
42 changes: 42 additions & 0 deletions src/db/db.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
import path from "node:path";
import { env } from "$utils/env.ts";
import { drizzle } from "drizzle-orm/postgres-js";
import { migrate } from "drizzle-orm/postgres-js/migrator";
import postgres from "postgres";

// biome-ignore lint/style/noNamespaceImport: Drizzle requies a namespace export
import * as guildConfig from "$db/schema/guild_config.ts";
// biome-ignore lint/style/noNamespaceImport: Drizzle requies a namespace export
import * as userConfig from "$db/schema/user_config.ts";

// __dirname replacement in ESM
const pathDirname = path.dirname(Bun.fileURLToPath(import.meta.url));
const DRIZZLE_DIRECTORY = path.join(pathDirname, "../../drizzle");

const pg = postgres({
max: 1,
host: env.POSTGRES_HOST,
port: Number.parseInt(env.POSTGRES_PORT),
user: env.POSTGRES_USER,
password: env.POSTGRES_PASSWORD,
database: env.POSTGRES_DB,

// Sends annoying notices to the shadow realm
onnotice: () => {
return;
},
});

const db = drizzle(pg, {
schema: {
...userConfig,
...guildConfig,
},
});

// Runs migrations
await migrate(db, {
migrationsFolder: DRIZZLE_DIRECTORY,
});

export { db };
56 changes: 56 additions & 0 deletions src/db/lib/guild_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { db } from "$db/db.ts";
import { guildConfig } from "$db/schema/guild_config.ts";
import type { HibikiGuildConfig } from "$db/typings/index.d.ts";
import { eq } from "drizzle-orm";

// Gets a guild config
export async function getGuildConfig(guild: string) {
try {
const config = await db.query.guildConfig.findFirst({
where: (guildConfig, { eq }) => eq(guildConfig.guild_id, guild),
});

if (!config?.guild_id) {
return;
}

return config;
} catch (error) {
throw new Error(Bun.inspect(error));
}
}

// Deletes a guild config
export async function deleteGuildConfig(guild: string) {
try {
await db.transaction(async (query) => {
await query.delete(guildConfig).where(eq(guildConfig.guild_id, guild));
});
} catch (error) {
throw new Error(Bun.inspect(error));
}
}

// Updates or inserts a guild config
export async function updateGuildConfig(guild: string, config: HibikiGuildConfig) {
try {
// Checks for an existing config
const existingConfig = await getGuildConfig(guild);

// Update if exists, else insert
await (existingConfig?.guild_id
? db.update(guildConfig).set(config).where(eq(guildConfig.guild_id, guild))
: db.insert(guildConfig).values(config).onConflictDoNothing());
} catch (error) {
throw new Error(Bun.inspect(error));
}
}

// Creates a blank guild config
export async function createBlankGuildConfig(guild: string) {
try {
await db.insert(guildConfig).values({ guild_id: guild }).onConflictDoNothing();
} catch (error) {
throw new Error(Bun.inspect(error));
}
}
56 changes: 56 additions & 0 deletions src/db/lib/user_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,56 @@
import { db } from "$db/db.ts";
import { userConfig } from "$db/schema/user_config.ts";
import type { HibikiUserConfig } from "$db/typings/index.d.ts";
import { eq } from "drizzle-orm";

// Gets a user config
export async function getUserConfig(user: string) {
try {
const config = await db.query.userConfig.findFirst({
where: (userConfig, { eq }) => eq(userConfig.user_id, user),
});

if (!config?.user_id) {
return;
}

return config;
} catch (error) {
throw new Error(Bun.inspect(error));
}
}

// Deletes a user config
export async function deleteUserConfig(user: string) {
try {
await db.transaction(async (query) => {
await query.delete(userConfig).where(eq(userConfig.user_id, user));
});
} catch (error) {
throw new Error(Bun.inspect(error));
}
}

// Updates or inserts a user config
export async function updateUserConfig(user: string, config: HibikiUserConfig) {
try {
// Checks for an existing config
const existingConfig = await getUserConfig(user);

// Update if exists, else insert
await (existingConfig?.user_id
? db.update(userConfig).set(config).where(eq(userConfig.user_id, user))
: db.insert(userConfig).values(config).onConflictDoNothing());
} catch (error) {
throw new Error(Bun.inspect(error));
}
}

// Creates a blank user config
export async function createBlankUserConfig(user: string) {
try {
await db.insert(userConfig).values({ user_id: user }).onConflictDoNothing();
} catch (error) {
throw new Error(Bun.inspect(error));
}
}
6 changes: 6 additions & 0 deletions src/db/schema/guild_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core";

export const guildConfig = pgTable("guild_config", {
id: uuid("id").primaryKey().defaultRandom(),
guild_id: text("guild_id").notNull(),
});
6 changes: 6 additions & 0 deletions src/db/schema/user_config.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import { pgTable, text, uuid } from "drizzle-orm/pg-core";

export const userConfig = pgTable("user_config", {
id: uuid("id").primaryKey().defaultRandom(),
user_id: text("user_id").notNull(),
});
8 changes: 8 additions & 0 deletions src/db/typings/index.d.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
export interface HibikiGuildConfig {
guild_id: string;
}

export interface HibikiUserConfig {
user_id: string;
locale?: string;
}
7 changes: 7 additions & 0 deletions src/utils/env.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,13 @@ const envSchema = z.object({
API_GOOGLEMAPS_KEY: z.string().trim().optional(),
API_GITHUB_PAT: z.string().trim().optional(),

// PostgreSQL options
POSTGRES_USER: z.string().trim().min(1, { message: "Missing PostgreSQL user" }),
POSTGRES_PASSWORD: z.string().trim().min(1, { message: "Missing PostgreSQL password" }),
POSTGRES_PORT: z.string().trim().min(1, { message: "Missing PostgreSQL port" }),
POSTGRES_HOST: z.string().trim().min(1, { message: "Missing PostgreSQL host" }),
POSTGRES_DB: z.string().trim().min(1, { message: "Missing PostgreSQL DB" }),

// Node/Bun stuff
NODE_ENV: z.string().default("DEVELOPMENT"),
npm_package_name: z.string().default("develop"),
Expand Down
Loading

0 comments on commit 1c8a606

Please sign in to comment.