diff --git a/config/environment.ts b/config/environment.ts index 10461f9..c6d357b 100644 --- a/config/environment.ts +++ b/config/environment.ts @@ -14,11 +14,13 @@ export const redisTtl: number = process.env.REDIS_TTL export const cassandra: CassandraConfig = { host: process.env.CASSANDRA_HOST || "localhost", port: Number.parseInt(process.env.CASSANDRA_PORT || "9042", 10), - keyspace: process.env.CASSANDRA_KEYSPACE || "", + keyspace: process.env.CASSANDRA_KEYSPACE || "void_db", username: process.env.CASSANDRA_USERNAME || "", password: process.env.CASSANDRA_PASSWORD || "", datacenter: process.env.CASSANDRA_DATACENTER || "", - contactPoints: (process.env.CASSANDRA_CONTACT_POINTS || "localhost").split(","), + contactPoints: (process.env.CASSANDRA_CONTACT_POINTS || "localhost").split( + ",", + ), authEnabled: process.env.CASSANDRA_AUTH_ENABLED === "false", }; diff --git a/config/setup/index.ts b/config/setup/index.ts new file mode 100644 index 0000000..fc9f954 --- /dev/null +++ b/config/setup/index.ts @@ -0,0 +1,68 @@ +import { readdir } from "node:fs/promises"; +import { extname, join, resolve } from "node:path"; +import { pathToFileURL } from "node:url"; +import { + cassandra as cassandraConfig, + verifyRequiredVariables, +} from "@config/environment"; +import { logger } from "@creations.works/logger"; +import { CassandraService } from "@lib/cassandra"; + +async function setup(): Promise { + verifyRequiredVariables(); + await CassandraService.connect({ withKeyspace: false }); + + const client = CassandraService.getClient(); + const keyspace = cassandraConfig.keyspace; + + if (!keyspace) { + logger.error("No Cassandra keyspace configured in environment."); + process.exit(1); + } + + await client.execute(` + CREATE KEYSPACE IF NOT EXISTS ${keyspace} + WITH REPLICATION = { + 'class': 'SimpleStrategy', + 'replication_factor': 1 + }; + `); + + await client.execute(`USE ${keyspace}`); + logger.info(`Keyspace "${keyspace}" ensured and selected.`); + + const tablesDir = resolve("config", "setup", "tables"); + const files = await readdir(tablesDir); + + for (const file of files) { + if (extname(file) !== ".ts") continue; + + const modulePath = pathToFileURL(join(tablesDir, file)).href; + const mod = await import(modulePath); + + if (typeof mod.default === "function") { + await mod.default(); + logger.info(`Ran default export from ${file}`); + } else if (typeof mod.createTable === "function") { + await mod.createTable(); + logger.info(`Ran createTable from ${file}`); + } else { + logger.warn(`No callable export found in ${file}`); + } + } + + logger.info("Setup complete."); +} + +setup() + .catch((error: Error) => { + logger.error(error); + process.exit(1); + }) + .finally(() => { + CassandraService.shutdown().catch((error: Error) => { + logger.error(["Error shutting down Cassandra client:", error as Error]); + }); + + process.exit(0); + }); diff --git a/config/setup/tables/users.ts b/config/setup/tables/users.ts new file mode 100644 index 0000000..82b132c --- /dev/null +++ b/config/setup/tables/users.ts @@ -0,0 +1,17 @@ +import { CassandraService } from "@lib/cassandra"; + +export async function createTable() { + await CassandraService.getClient().execute(` + CREATE TABLE IF NOT EXISTS users ( + id TEXT PRIMARY KEY, + name TEXT, + display_name TEXT, + email TEXT, + password TEXT, + avatar_url TEXT, + is_verified BOOLEAN, + created_at TIMESTAMP, + updated_at TIMESTAMP + ); + `); +} diff --git a/package.json b/package.json index a2517f3..a3f3ce1 100644 --- a/package.json +++ b/package.json @@ -7,7 +7,9 @@ "dev": "bun run --hot src/index.ts --dev", "lint": "bunx biome check", "lint:fix": "bunx biome check --fix", - "cleanup": "rm -rf logs node_modules bun.lockdb" + "cleanup": "rm -rf logs node_modules bun.lockdb", + + "setup": "bun run config/setup/index.ts" }, "devDependencies": { "@types/bun": "^1.2.11", diff --git a/src/index.ts b/src/index.ts index a3acbf1..3d62bbc 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,7 +1,7 @@ -import { CassandraService } from "@/lib/cassandra"; import { serverHandler } from "@/server"; import { verifyRequiredVariables } from "@config/environment"; import { logger } from "@creations.works/logger"; +import { CassandraService } from "@lib/cassandra"; import { redis } from "bun"; async function main(): Promise { diff --git a/src/lib/cassandra.ts b/src/lib/cassandra.ts index a533c4d..02a5acb 100644 --- a/src/lib/cassandra.ts +++ b/src/lib/cassandra.ts @@ -9,26 +9,31 @@ class CassandraService { public static getClient(): Client { if (!CassandraService.instance) { - CassandraService.instance = new Client({ - contactPoints: config.contactPoints, - localDataCenter: config.datacenter, - keyspace: config.keyspace, - authProvider: config.authEnabled - ? new auth.PlainTextAuthProvider(config.username, config.password) - : undefined, - protocolOptions: { - port: config.port, - }, - }); + logger.error("Cassandra client is not initialized somehow?"); + process.exit(1); } - return CassandraService.instance; } - public static async connect(): Promise { + public static async connect({ withKeyspace = true } = {}): Promise { + if (CassandraService.instance) return; + + const client = new Client({ + contactPoints: config.contactPoints, + localDataCenter: config.datacenter, + authProvider: config.authEnabled + ? new auth.PlainTextAuthProvider(config.username, config.password) + : undefined, + protocolOptions: { + port: config.port, + }, + ...(withKeyspace && config.keyspace ? { keyspace: config.keyspace } : {}), + }); + try { - await CassandraService.getClient().connect(); + await client.connect(); logger.info("Connected to Cassandra successfully."); + CassandraService.instance = client; } catch (error) { logger.error(["Failed to connect to Cassandra:", error as Error]); process.exit(1);