From b5bc7f6dafb37b4131c6eed9ca586762ac345c9c Mon Sep 17 00:00:00 2001 From: simoleo89 Date: Sat, 27 Jun 2026 15:26:40 +0200 Subject: [PATCH] Rebuild atomcms-next foundation on Prisma (align to habbo-next reference) Replace the superseded Drizzle monorepo scaffold with a single-app Prisma 7 data layer, mirroring the proven habbo-next approach to the shared Arcturus emulator DB (introspect/conform only, idempotent SQL migrations via a custom runner; never migrate-diff). - prisma/schema.prisma: User, UsersCurrency (composite PK), Ban, WebsiteSetting mapped to AtomCMS's default.sql columns; client generated to src/generated/prisma - src/lib/prisma.ts: PrismaMariaDb adapter singleton; src/env.ts zod env - src/lib/services: siteSettings (cached setting() equivalent) + CurrencyType enum - prisma/migrations + scripts/apply-migrations.ts: idempotent CMS-table runner Verified: prisma generate OK, tsc --noEmit exit 0, vitest 6/6 pass. --- .env.example | 14 +- .gitignore | 3 +- package.json | 24 +- packages/db/README.md | 24 - packages/db/drizzle.config.ts | 12 - packages/db/package.json | 23 - packages/db/src/bans/is-ban-active.test.ts | 22 - packages/db/src/bans/is-ban-active.ts | 5 - packages/db/src/client.ts | 16 - packages/db/src/index.ts | 8 - packages/db/src/integration/reads.test.ts | 47 - packages/db/src/repositories/bans-repo.ts | 13 - packages/db/src/repositories/currency-repo.ts | 16 - packages/db/src/repositories/settings-repo.ts | 15 - packages/db/src/repositories/users-repo.ts | 20 - packages/db/src/schema/bans.ts | 16 - packages/db/src/schema/index.ts | 4 - packages/db/src/schema/users-currency.ts | 13 - packages/db/src/schema/users.ts | 25 - packages/db/src/schema/website-settings.ts | 10 - packages/db/tsconfig.json | 5 - packages/db/vitest.config.ts | 9 - packages/settings/package.json | 16 - packages/settings/src/index.ts | 3 - .../settings/src/settings-service.test.ts | 39 - packages/settings/src/settings-service.ts | 41 - packages/settings/tsconfig.json | 5 - packages/settings/vitest.config.ts | 9 - pnpm-lock.yaml | 874 ++++++++++++++++-- pnpm-workspace.yaml | 3 - prisma.config.ts | 16 + .../0001_create_website_settings.sql | 10 + prisma/schema.prisma | 102 ++ scripts/apply-migrations.ts | 150 +++ src/env.ts | 20 + src/lib/prisma.ts | 27 + .../lib/services/currency.test.ts | 4 +- .../lib/services/currency.ts | 2 +- src/lib/services/site-settings.test.ts | 46 + src/lib/services/site-settings.ts | 39 + tsconfig.base.json => tsconfig.json | 16 +- turbo.json | 8 - vitest.config.ts | 15 + vitest.workspace.ts | 1 - 44 files changed, 1279 insertions(+), 511 deletions(-) delete mode 100644 packages/db/README.md delete mode 100644 packages/db/drizzle.config.ts delete mode 100644 packages/db/package.json delete mode 100644 packages/db/src/bans/is-ban-active.test.ts delete mode 100644 packages/db/src/bans/is-ban-active.ts delete mode 100644 packages/db/src/client.ts delete mode 100644 packages/db/src/index.ts delete mode 100644 packages/db/src/integration/reads.test.ts delete mode 100644 packages/db/src/repositories/bans-repo.ts delete mode 100644 packages/db/src/repositories/currency-repo.ts delete mode 100644 packages/db/src/repositories/settings-repo.ts delete mode 100644 packages/db/src/repositories/users-repo.ts delete mode 100644 packages/db/src/schema/bans.ts delete mode 100644 packages/db/src/schema/index.ts delete mode 100644 packages/db/src/schema/users-currency.ts delete mode 100644 packages/db/src/schema/users.ts delete mode 100644 packages/db/src/schema/website-settings.ts delete mode 100644 packages/db/tsconfig.json delete mode 100644 packages/db/vitest.config.ts delete mode 100644 packages/settings/package.json delete mode 100644 packages/settings/src/index.ts delete mode 100644 packages/settings/src/settings-service.test.ts delete mode 100644 packages/settings/src/settings-service.ts delete mode 100644 packages/settings/tsconfig.json delete mode 100644 packages/settings/vitest.config.ts delete mode 100644 pnpm-workspace.yaml create mode 100644 prisma.config.ts create mode 100644 prisma/migrations/0001_create_website_settings.sql create mode 100644 prisma/schema.prisma create mode 100644 scripts/apply-migrations.ts create mode 100644 src/env.ts create mode 100644 src/lib/prisma.ts rename packages/settings/src/currency-type.test.ts => src/lib/services/currency.test.ts (80%) rename packages/settings/src/currency-type.ts => src/lib/services/currency.ts (83%) create mode 100644 src/lib/services/site-settings.test.ts create mode 100644 src/lib/services/site-settings.ts rename tsconfig.base.json => tsconfig.json (53%) delete mode 100644 turbo.json create mode 100644 vitest.config.ts delete mode 100644 vitest.workspace.ts diff --git a/.env.example b/.env.example index 622bf604..1587e7c0 100644 --- a/.env.example +++ b/.env.example @@ -1,3 +1,11 @@ -# Connection to the LIVE/COPY emulator MySQL database (introspect-only). -# Format: mysql://user:password@host:3306/database -DATABASE_URL= +# Connection to the LIVE/COPY emulator MySQL/MariaDB database. +# The schema is owned by the Arcturus emulator — this app reads/writes data, +# it does NOT own or migrate the emulator tables. Format: +DATABASE_URL=mysql://user:password@127.0.0.1:3306/atomcms + +# Optional pool tuning (defaults shown) +DATABASE_POOL_SIZE=40 +DATABASE_IDLE_TIMEOUT_MS=300000 + +# Used by SSO ticket generation ({HOTEL_NAME}-{uuid}) +HOTEL_NAME=Atom diff --git a/.gitignore b/.gitignore index 36862c60..f8533d6e 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ node_modules/ -dist/ .turbo/ .env *.tsbuildinfo +# Prisma client is generated by `prisma generate` +src/generated/ diff --git a/package.json b/package.json index d0a7d06d..5dc1e619 100644 --- a/package.json +++ b/package.json @@ -5,18 +5,26 @@ "engines": { "node": ">=22" }, "packageManager": "pnpm@10.33.4", "scripts": { - "build": "turbo run build", - "test": "turbo run test", - "typecheck": "turbo run typecheck" + "prisma:generate": "prisma generate", + "typecheck": "tsc --noEmit", + "test": "vitest run", + "db:migrate": "tsx scripts/apply-migrations.ts", + "db:migrate:status": "tsx scripts/apply-migrations.ts --status" + }, + "dependencies": { + "@prisma/adapter-mariadb": "^7.8.0", + "@prisma/client": "^7.8.0", + "zod": "^3.24.0" }, "devDependencies": { - "turbo": "^2.3.0", - "typescript": "^5.7.0", + "@types/node": "^22.10.0", + "dotenv": "^16.4.0", + "prisma": "^7.8.0", "tsx": "^4.19.0", - "vitest": "^2.1.0", - "@types/node": "^22.10.0" + "typescript": "^5.7.0", + "vitest": "^2.1.0" }, "pnpm": { - "onlyBuiltDependencies": ["esbuild"] + "onlyBuiltDependencies": ["esbuild", "prisma", "@prisma/client", "@prisma/engines"] } } diff --git a/packages/db/README.md b/packages/db/README.md deleted file mode 100644 index 742af7c4..00000000 --- a/packages/db/README.md +++ /dev/null @@ -1,24 +0,0 @@ -# @atomcms/db - -Typed data-access layer over the **emulator-owned** MySQL database. - -## Hard rule: introspect-only - -The database schema is owned and written live by the Habbo game emulator. -**Never** run `drizzle-kit push`, `generate`, or `migrate` against it. The only -allowed schema command is introspection: - - DATABASE_URL=mysql://user:pass@host:3306/db pnpm --filter @atomcms/db db:pull - -`db:pull` reads the live schema and writes a snapshot to `./drizzle`. The -hand-authored files in `src/schema/` cover only the tables the web tier needs -first (users, users_currency, bans, website_settings); regenerate the full -122-table set from the live DB with `db:pull` when you need more tables. - -## Usage - - import { createDb, makeUsersRepo } from "@atomcms/db"; - const { db, pool } = createDb(process.env.DATABASE_URL!); - const users = makeUsersRepo(db); - const sysacc = await users.findByUsername("Systemaccount"); - await pool.end(); diff --git a/packages/db/drizzle.config.ts b/packages/db/drizzle.config.ts deleted file mode 100644 index 40417a8d..00000000 --- a/packages/db/drizzle.config.ts +++ /dev/null @@ -1,12 +0,0 @@ -import { defineConfig } from "drizzle-kit"; - -// INTROSPECT-ONLY. We never generate or push migrations against this database — -// it is owned and written live by the game emulator. Use only `drizzle-kit pull`. -export default defineConfig({ - dialect: "mysql", - schema: "./src/schema/index.ts", - out: "./drizzle", // where `pull` writes the introspected schema snapshot - dbCredentials: { - url: process.env.DATABASE_URL ?? "", - }, -}); diff --git a/packages/db/package.json b/packages/db/package.json deleted file mode 100644 index a31caf5f..00000000 --- a/packages/db/package.json +++ /dev/null @@ -1,23 +0,0 @@ -{ - "name": "@atomcms/db", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "test": "vitest run", - "typecheck": "tsc --noEmit", - "db:pull": "drizzle-kit pull" - }, - "dependencies": { - "@atomcms/settings": "workspace:*", - "drizzle-orm": "^0.38.0", - "mysql2": "^3.11.0" - }, - "devDependencies": { - "drizzle-kit": "^0.30.0", - "typescript": "^5.7.0", - "vitest": "^2.1.0" - } -} diff --git a/packages/db/src/bans/is-ban-active.test.ts b/packages/db/src/bans/is-ban-active.test.ts deleted file mode 100644 index 9b63bfb2..00000000 --- a/packages/db/src/bans/is-ban-active.test.ts +++ /dev/null @@ -1,22 +0,0 @@ -import { describe, it, expect } from "vitest"; -import { isBanActive } from "./is-ban-active.js"; - -describe("isBanActive", () => { - const now = 1_700_000_000; // fixed unix seconds - - it("is active when ban_expire is in the future", () => { - expect(isBanActive({ banExpire: now + 100 }, now)).toBe(true); - }); - - it("is inactive when ban_expire is in the past", () => { - expect(isBanActive({ banExpire: now - 100 }, now)).toBe(false); - }); - - it("is inactive when ban_expire equals now (Laravel uses strict >)", () => { - expect(isBanActive({ banExpire: now }, now)).toBe(false); - }); - - it("is inactive for the default 0 expiry", () => { - expect(isBanActive({ banExpire: 0 }, now)).toBe(false); - }); -}); diff --git a/packages/db/src/bans/is-ban-active.ts b/packages/db/src/bans/is-ban-active.ts deleted file mode 100644 index 53c7a5b9..00000000 --- a/packages/db/src/bans/is-ban-active.ts +++ /dev/null @@ -1,5 +0,0 @@ -// Mirrors Laravel's active-ban query: where('ban_expire', '>', time()). -// ban_expire and now are both unix timestamps in seconds. -export function isBanActive(ban: { banExpire: number }, nowUnixSeconds: number): boolean { - return ban.banExpire > nowUnixSeconds; -} diff --git a/packages/db/src/client.ts b/packages/db/src/client.ts deleted file mode 100644 index d87f2558..00000000 --- a/packages/db/src/client.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { drizzle, type MySql2Database } from "drizzle-orm/mysql2"; -import mysql from "mysql2/promise"; -import * as schema from "./schema/index.js"; - -export type Database = MySql2Database; - -/** - * Creates a Drizzle client backed by a mysql2 pool. - * The connection targets the LIVE emulator-owned database; this layer - * only ever reads/writes data — it never runs migrations. - */ -export function createDb(databaseUrl: string): { db: Database; pool: mysql.Pool } { - const pool = mysql.createPool(databaseUrl); - const db = drizzle(pool, { schema, mode: "default" }); - return { db, pool }; -} diff --git a/packages/db/src/index.ts b/packages/db/src/index.ts deleted file mode 100644 index 62436154..00000000 --- a/packages/db/src/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -export { createDb } from "./client.js"; -export type { Database } from "./client.js"; -export * as schema from "./schema/index.js"; -export { isBanActive } from "./bans/is-ban-active.js"; -export { makeUsersRepo } from "./repositories/users-repo.js"; -export { makeCurrencyRepo } from "./repositories/currency-repo.js"; -export { makeBansRepo } from "./repositories/bans-repo.js"; -export { makeSettingsRepo } from "./repositories/settings-repo.js"; diff --git a/packages/db/src/integration/reads.test.ts b/packages/db/src/integration/reads.test.ts deleted file mode 100644 index 6fc8cde6..00000000 --- a/packages/db/src/integration/reads.test.ts +++ /dev/null @@ -1,47 +0,0 @@ -import { describe, it, expect, beforeAll, afterAll } from "vitest"; -import type { Pool } from "mysql2/promise"; -import { createDb, type Database } from "../client.js"; -import { makeUsersRepo } from "../repositories/users-repo.js"; -import { makeCurrencyRepo } from "../repositories/currency-repo.js"; -import { makeSettingsRepo } from "../repositories/settings-repo.js"; -import { SettingsService } from "@atomcms/settings"; - -const url = process.env.DATABASE_URL; - -// Skips cleanly when no real database is configured (e.g. local dev without DB). -// The connection is built in beforeAll so it is NOT created when the suite is -// skipped (a skipped describe still executes its body during collection). -describe.skipIf(!url)("DB reads against the real schema", () => { - let db: Database; - let pool: Pool; - - beforeAll(() => { - const created = createDb(url as string); - db = created.db; - pool = created.pool; - }); - - afterAll(async () => { - await pool.end(); - }); - - it("reads a user by username", async () => { - const user = await makeUsersRepo(db).findByUsername("Systemaccount"); - expect(user).not.toBeNull(); - expect(user?.username).toBe("Systemaccount"); - }); - - it("reads composite-key currency rows for a user", async () => { - const user = await makeUsersRepo(db).findByUsername("Systemaccount"); - const balances = await makeCurrencyRepo(db).forUser(user!.id); - // Map; presence is enough to prove composite-key reads work. - expect(balances).toBeInstanceOf(Map); - }); - - it("loads settings through SettingsService", async () => { - const repo = makeSettingsRepo(db); - const svc = new SettingsService(() => repo.all()); - const name = await svc.get("hotel_name", "unknown"); - expect(typeof name).toBe("string"); - }); -}); diff --git a/packages/db/src/repositories/bans-repo.ts b/packages/db/src/repositories/bans-repo.ts deleted file mode 100644 index 41eec684..00000000 --- a/packages/db/src/repositories/bans-repo.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { eq } from "drizzle-orm"; -import type { Database } from "../client.js"; -import { bans } from "../schema/bans.js"; -import { isBanActive } from "../bans/is-ban-active.js"; - -export function makeBansRepo(db: Database) { - return { - activeForUser: async (userId: number, nowUnixSeconds: number) => { - const rows = await db.select().from(bans).where(eq(bans.userId, userId)); - return rows.filter((b) => isBanActive(b, nowUnixSeconds)); - }, - }; -} diff --git a/packages/db/src/repositories/currency-repo.ts b/packages/db/src/repositories/currency-repo.ts deleted file mode 100644 index c3467cc8..00000000 --- a/packages/db/src/repositories/currency-repo.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { eq } from "drizzle-orm"; -import type { Database } from "../client.js"; -import { usersCurrency } from "../schema/users-currency.js"; - -export function makeCurrencyRepo(db: Database) { - return { - // Returns a map of currency type -> amount for one user. - forUser: async (userId: number): Promise> => { - const rows = await db - .select() - .from(usersCurrency) - .where(eq(usersCurrency.userId, userId)); - return new Map(rows.map((r) => [r.type, r.amount])); - }, - }; -} diff --git a/packages/db/src/repositories/settings-repo.ts b/packages/db/src/repositories/settings-repo.ts deleted file mode 100644 index 3faafd68..00000000 --- a/packages/db/src/repositories/settings-repo.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { Database } from "../client.js"; -import { websiteSettings } from "../schema/website-settings.js"; -import type { SettingRow } from "@atomcms/settings"; - -export function makeSettingsRepo(db: Database) { - return { - // Shaped to match @atomcms/settings SettingsFetcher. - all: async (): Promise => { - const rows = await db - .select({ key: websiteSettings.key, value: websiteSettings.value }) - .from(websiteSettings); - return rows; - }, - }; -} diff --git a/packages/db/src/repositories/users-repo.ts b/packages/db/src/repositories/users-repo.ts deleted file mode 100644 index e7ac5ac4..00000000 --- a/packages/db/src/repositories/users-repo.ts +++ /dev/null @@ -1,20 +0,0 @@ -import { eq } from "drizzle-orm"; -import type { Database } from "../client.js"; -import { users } from "../schema/users.js"; - -export function makeUsersRepo(db: Database) { - return { - findById: async (id: number) => { - const rows = await db.select().from(users).where(eq(users.id, id)).limit(1); - return rows[0] ?? null; - }, - findByUsername: async (username: string) => { - const rows = await db - .select() - .from(users) - .where(eq(users.username, username)) - .limit(1); - return rows[0] ?? null; - }, - }; -} diff --git a/packages/db/src/schema/bans.ts b/packages/db/src/schema/bans.ts deleted file mode 100644 index 370ab57a..00000000 --- a/packages/db/src/schema/bans.ts +++ /dev/null @@ -1,16 +0,0 @@ -import { mysqlTable, int, varchar, mysqlEnum } from "drizzle-orm/mysql-core"; - -export const bans = mysqlTable("bans", { - id: int("id").autoincrement().primaryKey(), - userId: int("user_id").notNull(), - ip: varchar("ip", { length: 50 }).notNull().default(""), - machineId: varchar("machine_id", { length: 255 }).notNull().default(""), - userStaffId: int("user_staff_id").notNull(), - timestamp: int("timestamp").notNull(), - banExpire: int("ban_expire").notNull().default(0), - banReason: varchar("ban_reason", { length: 200 }).notNull().default(""), - type: mysqlEnum("type", ["account", "ip", "machine", "super"]) - .notNull() - .default("account"), - cfhTopic: int("cfh_topic").notNull().default(-1), -}); diff --git a/packages/db/src/schema/index.ts b/packages/db/src/schema/index.ts deleted file mode 100644 index 25a10c89..00000000 --- a/packages/db/src/schema/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { users } from "./users.js"; -export { usersCurrency } from "./users-currency.js"; -export { bans } from "./bans.js"; -export { websiteSettings } from "./website-settings.js"; diff --git a/packages/db/src/schema/users-currency.ts b/packages/db/src/schema/users-currency.ts deleted file mode 100644 index 4ac6690e..00000000 --- a/packages/db/src/schema/users-currency.ts +++ /dev/null @@ -1,13 +0,0 @@ -import { mysqlTable, int, primaryKey } from "drizzle-orm/mysql-core"; - -// MyISAM table with a composite primary key and no surrogate id column. -// This is the canonical test of the introspect-only / composite-key design. -export const usersCurrency = mysqlTable( - "users_currency", - { - userId: int("user_id").notNull(), - type: int("type").notNull(), - amount: int("amount").notNull().default(0), - }, - (t) => [primaryKey({ columns: [t.userId, t.type] })], -); diff --git a/packages/db/src/schema/users.ts b/packages/db/src/schema/users.ts deleted file mode 100644 index de771306..00000000 --- a/packages/db/src/schema/users.ts +++ /dev/null @@ -1,25 +0,0 @@ -import { mysqlTable, int, varchar, mysqlEnum } from "drizzle-orm/mysql-core"; - -// Subset of the real `users` table — the columns the web tier needs first. -// The complete column set will be regenerated via `db:pull` against the live DB. -export const users = mysqlTable("users", { - id: int("id").autoincrement().primaryKey(), - username: varchar("username", { length: 25 }).notNull(), - realName: varchar("real_name", { length: 25 }).notNull(), - password: varchar("password", { length: 64 }).notNull(), - mail: varchar("mail", { length: 500 }), - mailVerified: mysqlEnum("mail_verified", ["0", "1"]).notNull().default("0"), - accountCreated: int("account_created").notNull(), - lastLogin: int("last_login").notNull().default(0), - motto: varchar("motto", { length: 127 }).notNull().default(""), - look: varchar("look", { length: 256 }).notNull(), - gender: mysqlEnum("gender", ["M", "F"]).notNull().default("M"), - rank: int("rank").notNull().default(1), - credits: int("credits").notNull().default(2500), - pixels: int("pixels").notNull().default(500), - points: int("points").notNull().default(10), - online: mysqlEnum("online", ["0", "1", "2"]).notNull().default("0"), - authTicket: varchar("auth_ticket", { length: 256 }).notNull().default(""), - ipRegister: varchar("ip_register", { length: 45 }).notNull(), - ipCurrent: varchar("ip_current", { length: 45 }).notNull(), -}); diff --git a/packages/db/src/schema/website-settings.ts b/packages/db/src/schema/website-settings.ts deleted file mode 100644 index 9958c07f..00000000 --- a/packages/db/src/schema/website-settings.ts +++ /dev/null @@ -1,10 +0,0 @@ -import { mysqlTable, bigint, varchar, text } from "drizzle-orm/mysql-core"; - -// CMS-owned table (created by Laravel migration, not in default.sql). -// `value` was widened from varchar(255) to TEXT by a later migration. -export const websiteSettings = mysqlTable("website_settings", { - id: bigint("id", { mode: "number", unsigned: true }).autoincrement().primaryKey(), - key: varchar("key", { length: 255 }).notNull(), - value: text("value").notNull(), - comment: varchar("comment", { length: 255 }).notNull(), -}); diff --git a/packages/db/tsconfig.json b/packages/db/tsconfig.json deleted file mode 100644 index 6fcd57f7..00000000 --- a/packages/db/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist" }, - "include": ["src"] -} diff --git a/packages/db/vitest.config.ts b/packages/db/vitest.config.ts deleted file mode 100644 index 96d8a086..00000000 --- a/packages/db/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vitest/config"; - -// Local config so Vitest does not walk up into the parent Laravel repo's -// vite.config.js (this monorepo is nested inside the Laravel project). -export default defineConfig({ - test: { - environment: "node", - }, -}); diff --git a/packages/settings/package.json b/packages/settings/package.json deleted file mode 100644 index ec2a5744..00000000 --- a/packages/settings/package.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "name": "@atomcms/settings", - "version": "0.0.0", - "private": true, - "type": "module", - "main": "./src/index.ts", - "types": "./src/index.ts", - "scripts": { - "test": "vitest run", - "typecheck": "tsc --noEmit" - }, - "devDependencies": { - "typescript": "^5.7.0", - "vitest": "^2.1.0" - } -} diff --git a/packages/settings/src/index.ts b/packages/settings/src/index.ts deleted file mode 100644 index fa8cec89..00000000 --- a/packages/settings/src/index.ts +++ /dev/null @@ -1,3 +0,0 @@ -export { CurrencyType, currencyLabel } from "./currency-type.js"; -export { SettingsService } from "./settings-service.js"; -export type { SettingRow, SettingsFetcher } from "./settings-service.js"; diff --git a/packages/settings/src/settings-service.test.ts b/packages/settings/src/settings-service.test.ts deleted file mode 100644 index bb35078c..00000000 --- a/packages/settings/src/settings-service.test.ts +++ /dev/null @@ -1,39 +0,0 @@ -import { describe, it, expect, vi } from "vitest"; -import { SettingsService } from "./settings-service.js"; - -const rows = [ - { key: "hotel_name", value: "AtomHotel" }, - { key: "maintenance_enabled", value: "1" }, - { key: "radio_enabled", value: "0" }, -]; - -describe("SettingsService", () => { - it("returns a value by key", async () => { - const svc = new SettingsService(async () => rows); - expect(await svc.get("hotel_name")).toBe("AtomHotel"); - }); - - it("returns the default when key is missing", async () => { - const svc = new SettingsService(async () => rows); - expect(await svc.get("missing", "fallback")).toBe("fallback"); - expect(await svc.get("missing")).toBeNull(); - }); - - it("coerces '1'/'0' string booleans", async () => { - const svc = new SettingsService(async () => rows); - expect(await svc.getBool("maintenance_enabled")).toBe(true); - expect(await svc.getBool("radio_enabled")).toBe(false); - expect(await svc.getBool("missing", true)).toBe(true); - }); - - it("fetches only once and caches until cleared", async () => { - const fetcher = vi.fn(async () => rows); - const svc = new SettingsService(fetcher); - await svc.get("hotel_name"); - await svc.get("radio_enabled"); - expect(fetcher).toHaveBeenCalledTimes(1); - svc.clear(); - await svc.get("hotel_name"); - expect(fetcher).toHaveBeenCalledTimes(2); - }); -}); diff --git a/packages/settings/src/settings-service.ts b/packages/settings/src/settings-service.ts deleted file mode 100644 index 64b360e7..00000000 --- a/packages/settings/src/settings-service.ts +++ /dev/null @@ -1,41 +0,0 @@ -export interface SettingRow { - key: string; - value: string; -} - -export type SettingsFetcher = () => Promise; - -/** - * Mirrors Laravel's App\Services\SettingsService: loads the whole - * website_settings table into a key->value map, caches it in-process, - * and reads with a default fallback. Booleans are stored as '1'/'0'. - */ -export class SettingsService { - private cache: Map | null = null; - - constructor(private readonly fetcher: SettingsFetcher) {} - - private async load(): Promise> { - if (this.cache === null) { - const rows = await this.fetcher(); - this.cache = new Map(rows.map((r) => [r.key, r.value])); - } - return this.cache; - } - - async get(key: string, def: string | null = null): Promise { - const map = await this.load(); - return map.has(key) ? (map.get(key) as string) : def; - } - - async getBool(key: string, def = false): Promise { - const map = await this.load(); - if (!map.has(key)) return def; - const v = (map.get(key) as string).toLowerCase(); - return v === "1" || v === "true"; - } - - clear(): void { - this.cache = null; - } -} diff --git a/packages/settings/tsconfig.json b/packages/settings/tsconfig.json deleted file mode 100644 index 6fcd57f7..00000000 --- a/packages/settings/tsconfig.json +++ /dev/null @@ -1,5 +0,0 @@ -{ - "extends": "../../tsconfig.base.json", - "compilerOptions": { "rootDir": "src", "outDir": "dist" }, - "include": ["src"] -} diff --git a/packages/settings/vitest.config.ts b/packages/settings/vitest.config.ts deleted file mode 100644 index 96d8a086..00000000 --- a/packages/settings/vitest.config.ts +++ /dev/null @@ -1,9 +0,0 @@ -import { defineConfig } from "vitest/config"; - -// Local config so Vitest does not walk up into the parent Laravel repo's -// vite.config.js (this monorepo is nested inside the Laravel project). -export default defineConfig({ - test: { - environment: "node", - }, -}); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 53527f1c..d670d71b 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -7,16 +7,29 @@ settings: importers: .: + dependencies: + '@prisma/adapter-mariadb': + specifier: ^7.8.0 + version: 7.8.0 + '@prisma/client': + specifier: ^7.8.0 + version: 7.8.0(prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(typescript@5.9.3) + zod: + specifier: ^3.24.0 + version: 3.25.76 devDependencies: '@types/node': specifier: ^22.10.0 version: 22.20.0 + dotenv: + specifier: ^16.4.0 + version: 16.6.1 + prisma: + specifier: ^7.8.0 + version: 7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3) tsx: specifier: ^4.19.0 version: 4.22.4 - turbo: - specifier: ^2.3.0 - version: 2.10.0 typescript: specifier: ^5.7.0 version: 5.9.3 @@ -60,6 +73,20 @@ packages: '@drizzle-team/brocli@0.10.2': resolution: {integrity: sha512-z33Il7l5dKjUgGULTqBsQBQwckHh5AbIuxhdsIxDDiZAzBOrZO6q9ogcWC65kU382AfynTfgNumVcNIjuIua6w==} + '@electric-sql/pglite-socket@0.1.1': + resolution: {integrity: sha512-p2hoXw3Z3LQHwTeikdZNsFBOvXGqKY2hk51BBw+8NKND8eoH+8LFOtW9Z8CQKmTJ2qqGYu82ipqiyFZOTTXNfw==} + hasBin: true + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1': + resolution: {integrity: sha512-C+T3oivmy9bpQvSxVqXA1UDY8cB9Eb9vZHL9zxWwEUfDixbXv4G3r2LjoTdR33LD8aomR3O9ZXEO3XEwr/cUCA==} + peerDependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': + resolution: {integrity: sha512-mZ9NzzUSYPOCnxHH1oAHPRzoMFJHY472raDKwXl/+6oPbpdJ7g8LsCN4FSaIIfkiCKHhb3iF/Zqo3NYxaIhU7Q==} + '@esbuild-kit/core-utils@3.3.2': resolution: {integrity: sha512-sPRAnw9CdSsRmEtnsl2WXWdyquogVpB3yZ3dgwJfe8zrOzTsV7cJvmwrKVa+0ma5BoiGJ+BoqkMvawbayKUsqQ==} deprecated: 'Merged into tsx: https://tsx.is' @@ -632,12 +659,158 @@ packages: cpu: [x64] os: [win32] + '@hono/node-server@1.19.11': + resolution: {integrity: sha512-dr8/3zEaB+p0D2n/IUrlPF1HZm586qgJNXK1a9fhg/PzdtkK7Ksd5l312tJX2yBuALqDYBlG20QEbayqPyxn+g==} + engines: {node: '>=18.14.1'} + peerDependencies: + hono: ^4 + '@jridgewell/sourcemap-codec@1.5.5': resolution: {integrity: sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==} + '@kurkle/color@0.3.4': + resolution: {integrity: sha512-M5UknZPHRu3DEDWoipU6sE8PdkZ6Z/S+v4dD+Ke8IaNlpdSQah50lz1KtcFBa2vsdOnwbbnxJwVM4wty6udA5w==} + '@petamoriken/float16@3.9.3': resolution: {integrity: sha512-8awtpHXCx/bNpFt4mt2xdkgtgVvKqty8VbjHI/WWWQuEw+KLzFot3f4+LkQY9YmOtq7A5GdOnqoIC8Pdygjk2g==} + '@prisma/adapter-mariadb@7.8.0': + resolution: {integrity: sha512-mWsgcfbUjxB3qSzRlLs8E03vsKrqXzYK2zpx3e8u6wIgeHJM/sE46cuOGcYvHiZGmeQLCd3xL6YSSGM9QOLI6w==} + + '@prisma/client-runtime-utils@7.8.0': + resolution: {integrity: sha512-5NQZztQ0oY/ADFkmd9gPuweH5A1/CCY8YQPorLLO0Mu6a87mY5gsnDkzmFmIHs9NFaLnZojzgddFVN4RpKYrdw==} + + '@prisma/client@7.8.0': + resolution: {integrity: sha512-HFp3Dawv/3sU3JtlPha90IB+48lS7zHiH4LKZPjmcE8YH5P9DOXGPvo8dqOtO7MqLDd1p2hOWMcFlRT1DMblHw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + peerDependencies: + prisma: '*' + typescript: '>=5.4.0' + peerDependenciesMeta: + prisma: + optional: true + typescript: + optional: true + + '@prisma/config@7.8.0': + resolution: {integrity: sha512-HFESzd9rx2ZQxlK+TL7tu1HPvCqrHiL6LCxYykI2c34mvaUuIVVl3lYuicJD/MNnzgPnyeBEMlK4WTomJCV5jw==} + + '@prisma/debug@7.2.0': + resolution: {integrity: sha512-YSGTiSlBAVJPzX4ONZmMotL+ozJwQjRmZweQNIq/ER0tQJKJynNkRB3kyvt37eOfsbMCXk3gnLF6J9OJ4QWftw==} + + '@prisma/debug@7.8.0': + resolution: {integrity: sha512-p+QZReysDUqXC+mk17q9a+Y/qzh4c2KYliDK30buYUyfrGeTGSyfmc0AIrJRhZJrLHhRiJa9Au/J72h3C+szvA==} + + '@prisma/dev@0.24.3': + resolution: {integrity: sha512-ffHlQuKXZiaDt9Go0OnCTdJZrHxK0k7omJKNV86/VjpsXu5EIHZLK0T7JSWgvNlJwh56kW9JFu9v0qJciFzepg==} + + '@prisma/driver-adapter-utils@7.8.0': + resolution: {integrity: sha512-/Q13o0ZT0rjc1Xk0Q9KhZYwuq2EW/vSbWUBKfgEKkaCuB/Sg6bqnjmTZqC5cD4d6y1vfFAEwBRzfzoSMIVJ55A==} + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': + resolution: {integrity: sha512-fJPQxCkLgA5EayWaW8eArgCvjJ+N+Kz3VyeNKMEeYiQC4alNkxRKFVAGxv/ZUzuJISKqdw+zGeDbS6mn6RCPOA==} + + '@prisma/engines@7.8.0': + resolution: {integrity: sha512-jx3rCnNNrt5uzbkKlegtQ2GZHxSlihMCzutgT/BP6UIDF1r9tDI39hV/0T/cHZgzJ3ELbuQPXlVZy+Y1n0pcgw==} + + '@prisma/fetch-engine@7.8.0': + resolution: {integrity: sha512-gwB0Euiz/DDRyxFRpLXYlK3RfaZUj1c5dAYMuhZYfApg7arknJlcb9bIsOHDppJmbqYaVA+yBIiFMDBfprsNPQ==} + + '@prisma/get-platform@7.2.0': + resolution: {integrity: sha512-k1V0l0Td1732EHpAfi2eySTezyllok9dXb6UQanajkJQzPUGi3vO2z7jdkz67SypFTdmbnyGYxvEvYZdZsMAVA==} + + '@prisma/get-platform@7.8.0': + resolution: {integrity: sha512-WlxgRGnolL8VH2EmkH1R/DkKNr/mVdS3G2h42IZFFZ3eUrH9OT6t73kIOSlkkrv50wG123Iq8d96ufv5LlZktw==} + + '@prisma/query-plan-executor@7.2.0': + resolution: {integrity: sha512-EOZmNzcV8uJ0mae3DhTsiHgoNCuu1J9mULQpGCh62zN3PxPTd+qI9tJvk5jOst8WHKQNwJWR3b39t0XvfBB0WQ==} + + '@prisma/streams-local@0.1.2': + resolution: {integrity: sha512-l49yTxKKF2odFxaAXTmwmkBKL3+bVQ1tFOooGifu4xkdb9NMNLxHj27XAhTylWZod8I+ISGM5erU1xcl/oBCtg==} + engines: {bun: '>=1.3.6', node: '>=22.0.0'} + + '@prisma/studio-core@0.27.3': + resolution: {integrity: sha512-AADjNFPdsrglxHQVTmHFqv6DuKQZ5WY4p5/gVFY017twvNrSwpLJ9lqUbYYxEu2W7nbvVxTZA8deJ8LseNALsw==} + engines: {node: ^20.19 || ^22.12 || >=24.0, pnpm: '8'} + peerDependencies: + '@types/react': ^18.0.0 || ^19.0.0 + react: ^18.0.0 || ^19.0.0 + react-dom: ^18.0.0 || ^19.0.0 + + '@radix-ui/primitive@1.1.3': + resolution: {integrity: sha512-JTF99U/6XIjCBo0wqkU5sK10glYe27MRRsfwoiq5zzOEZLHU3A3KCMa5X/azekYRCJ0HlwI0crAXS/5dEHTzDg==} + + '@radix-ui/react-compose-refs@1.1.2': + resolution: {integrity: sha512-z4eqJvfiNnFMHIIvXP3CY57y2WJs5g2v3X0zm9mEJkrkNv4rDxu+sg9Jh8EkXyeqBkB7SOcboo9dMVqhyrACIg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-primitive@2.1.3': + resolution: {integrity: sha512-m9gTwRkhy2lvCPe6QJp4d3G1TYEUHn/FzJUtq9MjH46an1wJU+GdoGC5VLof8RX8Ft/DlpshApkhswDLZzHIcQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-slot@1.2.3': + resolution: {integrity: sha512-aeNmHnBxbi2St0au6VBVC7JXFlhLlOnvIIlePNniyUNAClzmtAUEY8/pBiK3iHjufOlwA+c20/8jngo7xcrg8A==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-toggle@1.1.10': + resolution: {integrity: sha512-lS1odchhFTeZv3xwHH31YPObmJn8gOg7Lq12inrr0+BH/l3Tsq32VfjqH1oh80ARM3mlkfMic15n0kg4sD1poQ==} + peerDependencies: + '@types/react': '*' + '@types/react-dom': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + react-dom: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@types/react-dom': + optional: true + + '@radix-ui/react-use-controllable-state@1.2.2': + resolution: {integrity: sha512-BjasUjixPFdS+NKkypcyyN5Pmg83Olst0+c6vGov0diwTEo6mgdqVR6hxcEgFuh4QrAs7Rc+9KuGJ9TVCj0Zzg==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-effect-event@0.0.2': + resolution: {integrity: sha512-Qp8WbZOBe+blgpuUT+lw2xheLP8q0oatc9UpmiemEICxGvFLYmHm9QowVZGHtJlGbS6A6yJ3iViad/2cVjnOiA==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + + '@radix-ui/react-use-layout-effect@1.1.1': + resolution: {integrity: sha512-RbJRS4UWQFkzHTTwVymMTUv8EqYhOp8dOOviLj2ugtTiXRaRQS7GLGxZTLL1jWhMeoSCf5zmcZkqTl9IiYfXcQ==} + peerDependencies: + '@types/react': '*' + react: ^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc + peerDependenciesMeta: + '@types/react': + optional: true + '@rollup/rollup-android-arm-eabi@4.62.2': resolution: {integrity: sha512-6o7ZLZK+BeenkZCFNDXqpbjw9bD6nuWonvS/lwQJp7NoVVxm6p3qE7qQ5jGuBjiFsgvqjD8mZAU5oWxTmbOeOg==} cpu: [arm] @@ -776,42 +949,24 @@ packages: cpu: [x64] os: [win32] - '@turbo/darwin-64@2.10.0': - resolution: {integrity: sha512-EwvHThXzpY0KGd1/NAmuewI5D+aVa3Rl/OlxE36yfjUKb/+ySrfJrSlEFt8aD1OXwnnaHnQnPKHFndor0Zxlsg==} - cpu: [x64] - os: [darwin] - - '@turbo/darwin-arm64@2.10.0': - resolution: {integrity: sha512-9d2fTyyG0lf5Wq1bwJA9qUaeecViMkLcdctWaMMmCkxZ/JqypmqOwK3W6vmejeKVgkr06gSoiX8bD+xN5Jpxcg==} - cpu: [arm64] - os: [darwin] - - '@turbo/linux-64@2.10.0': - resolution: {integrity: sha512-sZBtjMuufitanjzi6UssoUpJMnnPlLMcdcJj3m3ptNsSq31Xh7MnjhwA5nWvLDTfEFg8GPcbYFXMo8vSdKRfqQ==} - cpu: [x64] - os: [linux] - - '@turbo/linux-arm64@2.10.0': - resolution: {integrity: sha512-vkq/Z8R+1DQ+kifWFa810IjRy2NNBVvha3cg9sWA3nFh6nnGrHSMnnJKrzH7c/No9kq4Jb55Ru44YKsCSBgrKg==} - cpu: [arm64] - os: [linux] - - '@turbo/windows-64@2.10.0': - resolution: {integrity: sha512-CRUEguLWxFQHptYZS7HjPhNhAFawfea07iR+xAQ5e4klgLrPCMdexBkXwSCwOxqTFknJ7RZFN3gOaADsw+Gttg==} - cpu: [x64] - os: [win32] - - '@turbo/windows-arm64@2.10.0': - resolution: {integrity: sha512-dVHGaf9F8twzgibcBqKoADT/LLqf9++jDb+hq/LPWWaOmRpp4M+/pVOm7vy4z9D++xg8eaxWLT0+wQxFwhYu9A==} - cpu: [arm64] - os: [win32] + '@standard-schema/spec@1.1.0': + resolution: {integrity: sha512-l2aFy5jALhniG5HgqrD6jXLi/rUWrKvqN/qJx6yoJsgKhblVd+iqqU4RCXavm/jPityDo5TCvKMnpjKnOriy0w==} '@types/estree@1.0.9': resolution: {integrity: sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==} + '@types/geojson@7946.0.16': + resolution: {integrity: sha512-6C8nqWur3j98U6+lXDfTUWIfgvZU+EumvpHKcYjujKH7woYyLj2sUmff0tRhrqM7BohUw7Pz3ZB1jj2gW9Fvmg==} + '@types/node@22.20.0': resolution: {integrity: sha512-QWlFW2wf3nTjC13/DqRnBpR4ZO36VJH/JVBkA/vcnmbTBNQIlnObqyqZE1tUR7+Ni23Lda8R1BxMfbXRpCUx5g==} + '@types/node@24.13.2': + resolution: {integrity: sha512-fRa09kZTgu8o71KFcDjUFuc7F+dEbZYZmkI0mg5YBTRs0yMKjYHsq/c0urDKeDb+D5qVgXOdFcuu+DZPKOITwA==} + + '@types/react@19.2.17': + resolution: {integrity: sha512-MXfmqaVPEVgkBT/aY0aGCkRWWtByiYQXo3xdQ8r5RzuFrPiRn8Gar2tQdXSUQ2GKV3bkXckek89V8wQBY2Q/Aw==} + '@vitest/expect@2.1.9': resolution: {integrity: sha512-UJCIkTBenHeKT1TTlKMJWy1laZewsRIzYighyYiJKZreqtdxSos/S1t+ktRMQWu2CKqaarrkeszJx1cgC5tGZw==} @@ -841,6 +996,9 @@ packages: '@vitest/utils@2.1.9': resolution: {integrity: sha512-v0psaMSkNJ3A2NMrUEHFRzJtDPFn+/VWZ5WxImB21T9fjucJRmS7xCS3ppEnARb9y11OAzaD+P2Ps+b+BGX5iQ==} + ajv@8.20.0: + resolution: {integrity: sha512-Thbli+OlOj+iMPYFBVBfJ3OmCAnaSyNn4M1vz9T6Gka5Jt9ba/HIR56joy65tY6kx/FCF5VXNB819Y7/GUrBGA==} + assertion-error@2.0.1: resolution: {integrity: sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==} engines: {node: '>=12'} @@ -849,9 +1007,20 @@ packages: resolution: {integrity: sha512-NZKeq9AfyQvEeNlN0zSYAaWrmBffJh3IELMZfRpJVWgrpEbtEpnjvzqBPf+mxoI287JohRDoa+/nsfqqiZmF6g==} engines: {node: '>= 6.0.0'} + better-result@2.9.2: + resolution: {integrity: sha512-WIFoBPCdnTOdk9inkE1ZRvCZ4P0CpSkAiLlchC65N7n9DcjZ3NhqkBOlafzpOVnO8ixyi37kicmSJ3ENhPZl7Q==} + buffer-from@1.1.2: resolution: {integrity: sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==} + c12@3.3.4: + resolution: {integrity: sha512-cM0ApFQSBXuourJejzwv/AuPRvAxordTyParRVcHjjtXirtkzM0uK2L9TTn9s0cXZbG7E55jCivRQzoxYmRAlA==} + peerDependencies: + magicast: '*' + peerDependenciesMeta: + magicast: + optional: true + cac@6.7.14: resolution: {integrity: sha512-b6Ilus+c3RrdDk+JhLKUAQfzzgLEPy6wcXqS7f/xe1EETvsDP6GORG7SFuOs6cID5YkqchW/LXZbX5bc8j7ZcQ==} engines: {node: '>=8'} @@ -860,10 +1029,28 @@ packages: resolution: {integrity: sha512-4zNhdJD/iOjSH0A05ea+Ke6MU5mmpQcbQsSOkgdaUMJ9zTlDTD/GYlwohmIE2u0gaxHYiVHEn1Fw9mZ/ktJWgw==} engines: {node: '>=18'} + chart.js@4.5.1: + resolution: {integrity: sha512-GIjfiT9dbmHRiYi6Nl2yFCq7kkwdkp1W/lp2J99rX0yo9tgJGn3lKQATztIjb5tVtevcBtIdICNWqlq5+E8/Pw==} + engines: {pnpm: '>=8'} + check-error@2.1.3: resolution: {integrity: sha512-PAJdDJusoxnwm1VwW07VWwUN1sl7smmC3OKggvndJFadxxDRyFJBX/ggnu/KE4kQAB7a3Dp8f/YXC1FlUprWmA==} engines: {node: '>= 16'} + chokidar@5.0.0: + resolution: {integrity: sha512-TQMmc3w+5AxjpL8iIiwebF73dRDF4fBIieAqGn9RGCWaEVwQ6Fb2cGe31Yns0RRIzii5goJ1Y7xbMwo1TxMplw==} + engines: {node: '>= 20.19.0'} + + confbox@0.2.4: + resolution: {integrity: sha512-ysOGlgTFbN2/Y6Cg3Iye8YKulHw+R2fNXHrgSmXISQdMnomY6eNDprVdW9R5xBguEqI954+S6709UyiO7B+6OQ==} + + cross-spawn@7.0.6: + resolution: {integrity: sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==} + engines: {node: '>= 8'} + + csstype@3.2.3: + resolution: {integrity: sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==} + debug@4.4.3: resolution: {integrity: sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==} engines: {node: '>=6.0'} @@ -877,10 +1064,28 @@ packages: resolution: {integrity: sha512-h5k/5U50IJJFpzfL6nO9jaaumfjO/f2NjK/oYB2Djzm4p9L+3T9qWpZqZ2hAbLPuuYq9wrU08WQyBTL5GbPk5Q==} engines: {node: '>=6'} + deepmerge-ts@7.1.5: + resolution: {integrity: sha512-HOJkrhaYsweh+W+e74Yn7YStZOilkoPb6fycpwNLKzSPtruFs48nYis0zy5yJz1+ktUhHxoRDJ27RQAWLIJVJw==} + engines: {node: '>=16.0.0'} + + defu@6.1.7: + resolution: {integrity: sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==} + denque@2.1.0: resolution: {integrity: sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==} engines: {node: '>=0.10'} + destr@2.0.5: + resolution: {integrity: sha512-ugFTXCtDZunbzasqBxrK93Ik/DRYsO6S/fedkWEMKqt04xZ4csmnmwGDBAb07QWNaGMAmnTIemsYZCksjATwsA==} + + dotenv@16.6.1: + resolution: {integrity: sha512-uBq4egWHTcTt33a72vpSG0z3HnPuIl6NqYcTrKEg2azoEyl2hpW0zqlxysq2pK9HlDIHyHyakeYaYnSAwd8bow==} + engines: {node: '>=12'} + + dotenv@17.4.2: + resolution: {integrity: sha512-nI4U3TottKAcAD9LLud4Cb7b2QztQMUEfHbvhTH09bqXTxnSie8WnjPALV/WMCrJZ6UV/qHJ6L03OqO3LcdYZw==} + engines: {node: '>=12'} + drizzle-kit@0.30.6: resolution: {integrity: sha512-U4wWit0fyZuGuP7iNmRleQyK2V8wCuv57vf5l3MnG4z4fzNTjY/U13M8owyQ5RavqvqxBifWORaR3wIUzlN64g==} hasBin: true @@ -977,6 +1182,13 @@ packages: sqlite3: optional: true + effect@3.20.0: + resolution: {integrity: sha512-qMLfDJscrNG8p/aw+IkT9W7fgj50Z4wG5bLBy0Txsxz8iUHjDIkOgO3SV0WZfnQbNG2VJYb0b+rDLMrhM4+Krw==} + + empathic@2.0.0: + resolution: {integrity: sha512-i6UzDscO/XfAcNYD75CfICkmfLedpyPDdozrLMmQc5ORaQcdMoc21OnlEylMIqI7U8eniKrPMxxtj8k0vhmJhA==} + engines: {node: '>=14'} + env-paths@3.0.0: resolution: {integrity: sha512-dtJUTepzMW3Lm/NPxRf3wP4642UWhjL2sQxc+ym2YMj1m/H2zDNQOlezafzkHwn6sMstjHTwG6iQQsctDW/b1A==} engines: {node: ^12.20.0 || ^14.13.1 || >=16.0.0} @@ -1016,6 +1228,23 @@ packages: resolution: {integrity: sha512-KfYbmpRm0VbLjEvVa9yGwCi9GI34xvi7A/HXYWQO65CSD2u3MczUJSuwXKFIxlGsgBQizV9q5J9NHj4VG0n+pA==} engines: {node: '>=12.0.0'} + exsolve@1.1.0: + resolution: {integrity: sha512-D+42+T12DdIlJM3uepa55qGiL3sYdLBOxIl2ifQCzCHz4c7eiolaHsi3BIqEr7JxBzxv2pYZQX9kw16ziMcEmw==} + + fast-check@3.23.2: + resolution: {integrity: sha512-h5+1OzzfCC3Ef7VbtKdcv7zsstUQwUDlYpUTvjeUsJAssPgLn7QzbboPtL5ro04Mq0rPOsMzl7q5hIbRs2wD1A==} + engines: {node: '>=8.0.0'} + + fast-deep-equal@3.1.3: + resolution: {integrity: sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==} + + fast-uri@3.1.2: + resolution: {integrity: sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==} + + foreground-child@3.3.1: + resolution: {integrity: sha512-gIXjKqtFuWEgzFRJA9WCQeSJLZDjgJUOMCMzxtvFq/37KojM1BFGufqsCy0r4qSQmYLsZYMeyRqzIWOMup03sw==} + engines: {node: '>=14'} + fsevents@2.3.3: resolution: {integrity: sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==} engines: {node: ^8.16.0 || ^10.6.0 || >=11.0.0} @@ -1029,9 +1258,36 @@ packages: generate-function@2.3.1: resolution: {integrity: sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==} + get-port-please@3.2.0: + resolution: {integrity: sha512-I9QVvBw5U/hw3RmWpYKRumUeaDgxTPd401x364rLmWBJcOQ753eov1eTgzDqRG9bqFIfDc7gfzcQEWrUri3o1A==} + get-tsconfig@4.14.0: resolution: {integrity: sha512-yTb+8DXzDREzgvYmh6s9vHsSVCHeC0G3PI5bEXNBHtmshPnO+S5O7qgLEOn0I5QvMy6kpZN8K1NKGyilLb93wA==} + giget@3.3.0: + resolution: {integrity: sha512-gzi2D96p+AMfDcmJHGDj3KJ9NRiwvlFAU5yfa3ROwWZmFUjX4P43x3BcyRaOMMLto1vUo7C+86+MFhYTl6Ryiw==} + hasBin: true + + graceful-fs@4.2.11: + resolution: {integrity: sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==} + + grammex@3.1.12: + resolution: {integrity: sha512-6ufJOsSA7LcQehIJNCO7HIBykfM7DXQual0Ny780/DEcJIpBlHRvcqEBWGPYd7hrXL2GJ3oJI1MIhaXjWmLQOQ==} + + graphmatch@1.1.1: + resolution: {integrity: sha512-5ykVn/EXM1hF0XCaWh05VbYvEiOL2lY1kBxZtaYsyvjp7cmWOU1XsAdfQBwClraEofXDT197lFbXOEVMHpvQOg==} + + hono@4.12.27: + resolution: {integrity: sha512-1yrb/+w6HWQJrUCLkJ2IF5jNIPvvFkblV5RNOYl6bV+OA6p9GLcMpHFFGTosSvHvcAUibuUukRqhlYI4z32C7Q==} + engines: {node: '>=16.9.0'} + + http-status-codes@2.3.0: + resolution: {integrity: sha512-RJ8XvFvpPM/Dmc5SV+dC4y5PCeOhT3x1Hq0NU3rjGeg5a/CqlhZ7uudknPwZFz4aeAXDcbAyaeP7GAo9lvngtA==} + + iconv-lite@0.6.3: + resolution: {integrity: sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==} + engines: {node: '>=0.10.0'} + iconv-lite@0.7.2: resolution: {integrity: sha512-im9DjEDQ55s9fL4EYzOAv0yMqmMBSZp6G0VvFyTMPKWxiSBHUj9NW/qqLmXUwXrrM7AvqSlTCfvqRb0cM8yYqw==} engines: {node: '>=0.10.0'} @@ -1039,16 +1295,29 @@ packages: is-property@1.0.2: resolution: {integrity: sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g==} + isexe@2.0.0: + resolution: {integrity: sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==} + isexe@3.1.5: resolution: {integrity: sha512-6B3tLtFqtQS4ekarvLVMZ+X+VlvQekbe4taUkf/rhVO3d/h0M2rfARm/pXLcPEsjjMsFgrFgSrhQIxcSVrBz8w==} engines: {node: '>=18'} + jiti@2.7.0: + resolution: {integrity: sha512-AC/7JofJvZGrrneWNaEnJeOLUx+JlGt7tNa0wZiRPT4MY1wmfKjt2+6O2p2uz2+skll8OZZmJMNqeke7kKbNgQ==} + hasBin: true + + json-schema-traverse@1.0.0: + resolution: {integrity: sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==} + long@5.3.2: resolution: {integrity: sha512-mNAgZ1GmyNhD7AuqnTG3/VQ26o760+ZYBPKjPvugO8+nLbYfX6TVpJPseBvopbdY+qpZ/lKUnmEc1LeZYS3QAA==} loupe@3.2.1: resolution: {integrity: sha512-CdzqowRJCeLU72bHvWqwRBBlLcMEtIvGrlvef74kMnV2AolS9Y8xUv1I0U/MNAWMhBlKIoyuEgoJ0t/bbwHbLQ==} + lru-cache@10.4.3: + resolution: {integrity: sha512-JNAzZcXrCt42VGLuYz0zfAzDfAvJWW6AfYlDBQyDV5DClI2m5sAmK+OIO7s59XfsRsWHp02jAJrRadPRGTt6SQ==} + lru.min@1.1.4: resolution: {integrity: sha512-DqC6n3QQ77zdFpCMASA1a3Jlb64Hv2N2DciFGkO/4L9+q/IpIAuRlKOvCXabtRW6cQf8usbmM6BE/TOPysCdIA==} engines: {bun: '>=1.0.0', deno: '>=1.30.0', node: '>=8.0.0'} @@ -1056,9 +1325,17 @@ packages: magic-string@0.30.21: resolution: {integrity: sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==} + mariadb@3.4.5: + resolution: {integrity: sha512-gThTYkhIS5rRqkVr+Y0cIdzr+GRqJ9sA2Q34e0yzmyhMCwyApf3OKAC1jnF23aSlIOqJuyaUFUcj7O1qZslmmQ==} + engines: {node: '>= 14'} + ms@2.1.3: resolution: {integrity: sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==} + mysql2@3.15.3: + resolution: {integrity: sha512-FBrGau0IXmuqg4haEZRBfHNWB5mUARw6hNwPDXXGg0XzVJ50mr/9hb267lvpVMnhZ1FON3qNd4Xfcez1rbFwSg==} + engines: {node: '>= 8.0'} + mysql2@3.22.5: resolution: {integrity: sha512-95uZ2TrPWAZdwpB3vvvDbmEMcNG8yIeNCyu6GUcr/QnWEE/wXm7+mhOCsdQfWQDTV7qYT/PDUZ4U4UPP4AsXqQ==} engines: {node: '>= 8.0'} @@ -1074,23 +1351,89 @@ packages: engines: {node: ^10 || ^12 || ^13.7 || ^14 || >=15.0.1} hasBin: true + ohash@2.0.11: + resolution: {integrity: sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==} + + path-key@3.1.1: + resolution: {integrity: sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==} + engines: {node: '>=8'} + pathe@1.1.2: resolution: {integrity: sha512-whLdWMYL2TwI08hn8/ZqAbrVemu0LNaNNJZX73O6qaIdCTfXutsLhMkjdENX0qhsQ9uIimo4/aQOmXkoon2nDQ==} + pathe@2.0.3: + resolution: {integrity: sha512-WUjGcAqP1gQacoQe+OBJsFA7Ld4DyXuUIjZ5cc75cLHvJ7dtNsTugphxIADwspS+AraAUePCKrSVtPLFj/F88w==} + pathval@2.0.1: resolution: {integrity: sha512-//nshmD55c46FuFw26xV/xFAaB5HF9Xdap7HJBBnrKdAd6/GxDBaNA1870O79+9ueg61cZLSVc+OaFlfmObYVQ==} engines: {node: '>= 14.16'} + perfect-debounce@2.1.0: + resolution: {integrity: sha512-LjgdTytVFXeUgtHZr9WYViYSM/g8MkcTPYDlPa3cDqMirHjKiSZPYd6DoL7pK8AJQr+uWkQvCjHNdiMqsrJs+g==} + picocolors@1.1.1: resolution: {integrity: sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==} + pkg-types@2.3.1: + resolution: {integrity: sha512-y+ichcgc2LrADuhLNAx8DFjVfgz91pRxfZdI3UDhxHvcVEZsenLO+7XaU5vOp0u/7V/wZ+plyuQxtrDlZJ+yeg==} + postcss@8.5.15: resolution: {integrity: sha512-FfR8sjd4em2T6fb3I2MwAJU7HWVMr9zba+enmQeeWFfCbm+UOC/0X4DS8XtpUTMwWMGbjKYP7xjfNekzyGmB3A==} engines: {node: ^10 || ^12 || >=14} + postgres@3.4.7: + resolution: {integrity: sha512-Jtc2612XINuBjIl/QTWsV5UvE8UHuNblcO3vVADSrKsrc6RqGX6lOW1cEo3CM2v0XG4Nat8nI+YM7/f26VxXLw==} + engines: {node: '>=12'} + + prisma@7.8.0: + resolution: {integrity: sha512-yfN4yrw7HV9kEJhoy1+jgah0jafEIQsf7uWouSsM8MvJtlubsk+kM7AIBWZ8+GJl74Yj3c+nbYqBkMOxtsZ3Lw==} + engines: {node: ^20.19 || ^22.12 || >=24.0} + hasBin: true + peerDependencies: + better-sqlite3: '>=9.0.0' + typescript: '>=5.4.0' + peerDependenciesMeta: + better-sqlite3: + optional: true + typescript: + optional: true + + proper-lockfile@4.1.2: + resolution: {integrity: sha512-TjNPblN4BwAWMXU8s9AEz4JmQxnD1NNL7bNOY/AKUzyamc379FWASUhc/K1pL2noVb+XmZKLL68cjzLsiOAMaA==} + + pure-rand@6.1.0: + resolution: {integrity: sha512-bVWawvoZoBYpp6yIoQtQXHZjmz35RSVHnUOTefl8Vcjr8snTPY1wnpSPMWekcFwbxI6gtmT7rSYPFvz71ldiOA==} + + rc9@3.0.1: + resolution: {integrity: sha512-gMDyleLWVE+i6Sgtc0QbbY6pEKqYs97NGi6isHQPqYlLemPoO8dxQ3uGi0f4NiP98c+jMW6cG1Kx9dDwfvqARQ==} + + react-dom@19.2.7: + resolution: {integrity: sha512-t0BRVXvbiE/o20Hfw669rLbMCDWtYZLvmJigy2f0MxsXF+71pxhR3xOkspmsO8h3ZlNzyibAmtCa3l4lYKk6gQ==} + peerDependencies: + react: ^19.2.7 + + react@19.2.7: + resolution: {integrity: sha512-HNe9WslTbXmFK8o8cmwgAeJFSBvt1bPdHCVKtaaV+WlAN36mpT4hcRpwbf3fY56ar2oIXzsBpOAiIRHAdY0OlQ==} + engines: {node: '>=0.10.0'} + + readdirp@5.0.0: + resolution: {integrity: sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==} + engines: {node: '>= 20.19.0'} + + remeda@2.33.4: + resolution: {integrity: sha512-ygHswjlc/opg2VrtiYvUOPLjxjtdKvjGz1/plDhkG66hjNjFr1xmfrs2ClNFo/E6TyUFiwYNh53bKV26oBoMGQ==} + + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-pkg-maps@1.0.0: resolution: {integrity: sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==} + retry@0.12.0: + resolution: {integrity: sha512-9LkiTwjUh6rT555DtE9rTX+BKByPfrMzEAtnlEtdEwr3Nkffwiihqe2bWADg+OQRjt9gl6ICdmB/ZFDCGAtSow==} + engines: {node: '>= 4'} + rollup@4.62.2: resolution: {integrity: sha512-RFnrW4lhXA3s3eqHDZvN654g8OTjzRfqpIRJYczCGB6HzphckVAi/Qh4tbPUbRuDi7s1Llv8g/NspLkttY3gTA==} engines: {node: '>=18.0.0', npm: '>=8.0.0'} @@ -1099,11 +1442,25 @@ packages: safer-buffer@2.1.2: resolution: {integrity: sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==} + scheduler@0.27.0: + resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} + semver@7.8.5: resolution: {integrity: sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==} engines: {node: '>=10'} hasBin: true + seq-queue@0.0.5: + resolution: {integrity: sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q==} + + shebang-command@2.0.0: + resolution: {integrity: sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==} + engines: {node: '>=8'} + + shebang-regex@3.0.0: + resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} + engines: {node: '>=8'} + shell-quote@1.9.0: resolution: {integrity: sha512-Iov+JwFv/2HcTpcwNMKd8+IWNb8tboQJNQTkAY/LLVK7gGH9jy+LGkVqPxfekHl+yMmiqXszdGWXgkfml7hjqA==} engines: {node: '>= 0.4'} @@ -1111,6 +1468,13 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + signal-exit@3.0.7: + resolution: {integrity: sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==} + + signal-exit@4.1.0: + resolution: {integrity: sha512-bzyZ1e88w9O1iNJbKnOlvYTrWPDl46O1bG0D3XInv+9tkPrxrN8jUUTiFlDkkmKWgn1M6CfIA13SuGqOa9Korw==} + engines: {node: '>=14'} + source-map-js@1.2.1: resolution: {integrity: sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==} engines: {node: '>=0.10.0'} @@ -1126,6 +1490,10 @@ packages: resolution: {integrity: sha512-BsTCV265VpTp8tm1wyIm1xqQCS+Q9NHx2Sr+WcnUrgLrQ6yiDIvHYJV5gHxsj1lMBy2zm5twLaZao8Jd+S8JJw==} engines: {bun: '>=1.0.0', deno: '>=2.0.0', node: '>=12.0.0'} + sqlstring@2.3.3: + resolution: {integrity: sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==} + engines: {node: '>= 0.6'} + stackback@0.0.2: resolution: {integrity: sha512-1XMJE5fQo1jGH6Y/7ebnwPOBEkIEnT4QF32d5R1+VXdXveM0IBMJt8zfaxX1P3QhVwrYe+576+jkANtSS2mBbw==} @@ -1155,10 +1523,6 @@ packages: engines: {node: '>=18.0.0'} hasBin: true - turbo@2.10.0: - resolution: {integrity: sha512-o016H9PPtuH2deb3mh3Vci3Avfi9UYgM/RONQisY7HnloupP0IFSbFS3gFYJgFJP8nwBrByHWFQIDa8T2zIXPw==} - hasBin: true - typescript@5.9.3: resolution: {integrity: sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==} engines: {node: '>=14.17'} @@ -1167,6 +1531,17 @@ packages: undici-types@6.21.0: resolution: {integrity: sha512-iwDZqg0QAGrg9Rav5H4n0M64c3mkR59cJ6wQp+7C4nI0gsmExaedaYLNO44eT4AtBBwjbTiGPMlt2Md0T9H9JQ==} + undici-types@7.18.2: + resolution: {integrity: sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==} + + valibot@1.2.0: + resolution: {integrity: sha512-mm1rxUsmOxzrwnX5arGS+U4T25RdvpPjPN4yR0u9pUBov9+zGVtO84tif1eY4r6zWxVxu3KzIyknJy3rxfRZZg==} + peerDependencies: + typescript: '>=5' + peerDependenciesMeta: + typescript: + optional: true + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -1228,6 +1603,11 @@ packages: jsdom: optional: true + which@2.0.2: + resolution: {integrity: sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==} + engines: {node: '>= 8'} + hasBin: true + which@4.0.0: resolution: {integrity: sha512-GlaYyEb07DPxYCKhKzplCWBJtvxZcZMrL+4UkrTSJHHPyZU4mYYTv3qaOe77H7EODLSSopAUFAc6W8U4yqvscg==} engines: {node: ^16.13.0 || >=18.0.0} @@ -1238,10 +1618,26 @@ packages: engines: {node: '>=8'} hasBin: true + zeptomatch@2.1.0: + resolution: {integrity: sha512-KiGErG2J0G82LSpniV0CtIzjlJ10E04j02VOudJsPyPwNZgGnRKQy7I1R7GMyg/QswnE4l7ohSGrQbQbjXPPDA==} + + zod@3.25.76: + resolution: {integrity: sha512-gzUt/qt81nXsFGKIFcC3YnfEAx5NkunCfnDlvuBSSFS02bcXu4Lmea0AFIUwbLWxWPx3d9p8S5QoaujKcNQxcQ==} + snapshots: '@drizzle-team/brocli@0.10.2': {} + '@electric-sql/pglite-socket@0.1.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite-tools@0.3.1(@electric-sql/pglite@0.4.1)': + dependencies: + '@electric-sql/pglite': 0.4.1 + + '@electric-sql/pglite@0.4.1': {} + '@esbuild-kit/core-utils@3.3.2': dependencies: esbuild: 0.18.20 @@ -1534,10 +1930,165 @@ snapshots: '@esbuild/win32-x64@0.28.1': optional: true + '@hono/node-server@1.19.11(hono@4.12.27)': + dependencies: + hono: 4.12.27 + '@jridgewell/sourcemap-codec@1.5.5': {} + '@kurkle/color@0.3.4': {} + '@petamoriken/float16@3.9.3': {} + '@prisma/adapter-mariadb@7.8.0': + dependencies: + '@prisma/driver-adapter-utils': 7.8.0 + mariadb: 3.4.5 + + '@prisma/client-runtime-utils@7.8.0': {} + + '@prisma/client@7.8.0(prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3))(typescript@5.9.3)': + dependencies: + '@prisma/client-runtime-utils': 7.8.0 + optionalDependencies: + prisma: 7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3) + typescript: 5.9.3 + + '@prisma/config@7.8.0': + dependencies: + c12: 3.3.4 + deepmerge-ts: 7.1.5 + effect: 3.20.0 + empathic: 2.0.0 + transitivePeerDependencies: + - magicast + + '@prisma/debug@7.2.0': {} + + '@prisma/debug@7.8.0': {} + + '@prisma/dev@0.24.3(typescript@5.9.3)': + dependencies: + '@electric-sql/pglite': 0.4.1 + '@electric-sql/pglite-socket': 0.1.1(@electric-sql/pglite@0.4.1) + '@electric-sql/pglite-tools': 0.3.1(@electric-sql/pglite@0.4.1) + '@hono/node-server': 1.19.11(hono@4.12.27) + '@prisma/get-platform': 7.2.0 + '@prisma/query-plan-executor': 7.2.0 + '@prisma/streams-local': 0.1.2 + foreground-child: 3.3.1 + get-port-please: 3.2.0 + hono: 4.12.27 + http-status-codes: 2.3.0 + pathe: 2.0.3 + proper-lockfile: 4.1.2 + remeda: 2.33.4 + std-env: 3.10.0 + valibot: 1.2.0(typescript@5.9.3) + zeptomatch: 2.1.0 + transitivePeerDependencies: + - typescript + + '@prisma/driver-adapter-utils@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/engines-version@7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a': {} + + '@prisma/engines@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/fetch-engine': 7.8.0 + '@prisma/get-platform': 7.8.0 + + '@prisma/fetch-engine@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + '@prisma/engines-version': 7.8.0-6.3c6e192761c0362d496ed980de936e2f3cebcd3a + '@prisma/get-platform': 7.8.0 + + '@prisma/get-platform@7.2.0': + dependencies: + '@prisma/debug': 7.2.0 + + '@prisma/get-platform@7.8.0': + dependencies: + '@prisma/debug': 7.8.0 + + '@prisma/query-plan-executor@7.2.0': {} + + '@prisma/streams-local@0.1.2': + dependencies: + ajv: 8.20.0 + better-result: 2.9.2 + env-paths: 3.0.0 + proper-lockfile: 4.1.2 + + '@prisma/studio-core@0.27.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-toggle': 1.1.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@types/react': 19.2.17 + chart.js: 4.5.1 + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + transitivePeerDependencies: + - '@types/react-dom' + + '@radix-ui/primitive@1.1.3': {} + + '@radix-ui/react-compose-refs@1.1.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-primitive@2.1.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/react-slot': 1.2.3(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-slot@1.2.3(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-compose-refs': 1.1.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-toggle@1.1.10(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)': + dependencies: + '@radix-ui/primitive': 1.1.3 + '@radix-ui/react-primitive': 2.1.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + '@radix-ui/react-use-controllable-state': 1.2.2(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + react-dom: 19.2.7(react@19.2.7) + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-controllable-state@1.2.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-effect-event': 0.0.2(@types/react@19.2.17)(react@19.2.7) + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-effect-event@0.0.2(@types/react@19.2.17)(react@19.2.7)': + dependencies: + '@radix-ui/react-use-layout-effect': 1.1.1(@types/react@19.2.17)(react@19.2.7) + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + + '@radix-ui/react-use-layout-effect@1.1.1(@types/react@19.2.17)(react@19.2.7)': + dependencies: + react: 19.2.7 + optionalDependencies: + '@types/react': 19.2.17 + '@rollup/rollup-android-arm-eabi@4.62.2': optional: true @@ -1613,30 +2164,24 @@ snapshots: '@rollup/rollup-win32-x64-msvc@4.62.2': optional: true - '@turbo/darwin-64@2.10.0': - optional: true - - '@turbo/darwin-arm64@2.10.0': - optional: true - - '@turbo/linux-64@2.10.0': - optional: true - - '@turbo/linux-arm64@2.10.0': - optional: true - - '@turbo/windows-64@2.10.0': - optional: true - - '@turbo/windows-arm64@2.10.0': - optional: true + '@standard-schema/spec@1.1.0': {} '@types/estree@1.0.9': {} + '@types/geojson@7946.0.16': {} + '@types/node@22.20.0': dependencies: undici-types: 6.21.0 + '@types/node@24.13.2': + dependencies: + undici-types: 7.18.2 + + '@types/react@19.2.17': + dependencies: + csstype: 3.2.3 + '@vitest/expect@2.1.9': dependencies: '@vitest/spy': 2.1.9 @@ -1677,12 +2222,36 @@ snapshots: loupe: 3.2.1 tinyrainbow: 1.2.0 + ajv@8.20.0: + dependencies: + fast-deep-equal: 3.1.3 + fast-uri: 3.1.2 + json-schema-traverse: 1.0.0 + require-from-string: 2.0.2 + assertion-error@2.0.1: {} aws-ssl-profiles@1.1.2: {} + better-result@2.9.2: {} + buffer-from@1.1.2: {} + c12@3.3.4: + dependencies: + chokidar: 5.0.0 + confbox: 0.2.4 + defu: 6.1.7 + dotenv: 17.4.2 + exsolve: 1.1.0 + giget: 3.3.0 + jiti: 2.7.0 + ohash: 2.0.11 + pathe: 2.0.3 + perfect-debounce: 2.1.0 + pkg-types: 2.3.1 + rc9: 3.0.1 + cac@6.7.14: {} chai@5.3.3: @@ -1693,16 +2262,44 @@ snapshots: loupe: 3.2.1 pathval: 2.0.1 + chart.js@4.5.1: + dependencies: + '@kurkle/color': 0.3.4 + check-error@2.1.3: {} + chokidar@5.0.0: + dependencies: + readdirp: 5.0.0 + + confbox@0.2.4: {} + + cross-spawn@7.0.6: + dependencies: + path-key: 3.1.1 + shebang-command: 2.0.0 + which: 2.0.2 + + csstype@3.2.3: {} + debug@4.4.3: dependencies: ms: 2.1.3 deep-eql@5.0.2: {} + deepmerge-ts@7.1.5: {} + + defu@6.1.7: {} + denque@2.1.0: {} + destr@2.0.5: {} + + dotenv@16.6.1: {} + + dotenv@17.4.2: {} + drizzle-kit@0.30.6: dependencies: '@drizzle-team/brocli': 0.10.2 @@ -1717,6 +2314,13 @@ snapshots: optionalDependencies: mysql2: 3.22.5(@types/node@22.20.0) + effect@3.20.0: + dependencies: + '@standard-schema/spec': 1.1.0 + fast-check: 3.23.2 + + empathic@2.0.0: {} + env-paths@3.0.0: {} es-module-lexer@1.7.0: {} @@ -1840,6 +2444,21 @@ snapshots: expect-type@1.4.0: {} + exsolve@1.1.0: {} + + fast-check@3.23.2: + dependencies: + pure-rand: 6.1.0 + + fast-deep-equal@3.1.3: {} + + fast-uri@3.1.2: {} + + foreground-child@3.3.1: + dependencies: + cross-spawn: 7.0.6 + signal-exit: 4.1.0 + fsevents@2.3.3: optional: true @@ -1858,30 +2477,76 @@ snapshots: dependencies: is-property: 1.0.2 + get-port-please@3.2.0: {} + get-tsconfig@4.14.0: dependencies: resolve-pkg-maps: 1.0.0 + giget@3.3.0: {} + + graceful-fs@4.2.11: {} + + grammex@3.1.12: {} + + graphmatch@1.1.1: {} + + hono@4.12.27: {} + + http-status-codes@2.3.0: {} + + iconv-lite@0.6.3: + dependencies: + safer-buffer: 2.1.2 + iconv-lite@0.7.2: dependencies: safer-buffer: 2.1.2 is-property@1.0.2: {} + isexe@2.0.0: {} + isexe@3.1.5: {} + jiti@2.7.0: {} + + json-schema-traverse@1.0.0: {} + long@5.3.2: {} loupe@3.2.1: {} + lru-cache@10.4.3: {} + lru.min@1.1.4: {} magic-string@0.30.21: dependencies: '@jridgewell/sourcemap-codec': 1.5.5 + mariadb@3.4.5: + dependencies: + '@types/geojson': 7946.0.16 + '@types/node': 24.13.2 + denque: 2.1.0 + iconv-lite: 0.6.3 + lru-cache: 10.4.3 + ms@2.1.3: {} + mysql2@3.15.3: + dependencies: + aws-ssl-profiles: 1.1.2 + denque: 2.1.0 + generate-function: 2.3.1 + iconv-lite: 0.7.2 + long: 5.3.2 + lru.min: 1.1.4 + named-placeholders: 1.1.6 + seq-queue: 0.0.5 + sqlstring: 2.3.3 + mysql2@3.22.5(@types/node@22.20.0): dependencies: '@types/node': 22.20.0 @@ -1900,20 +2565,81 @@ snapshots: nanoid@3.3.15: {} + ohash@2.0.11: {} + + path-key@3.1.1: {} + pathe@1.1.2: {} + pathe@2.0.3: {} + pathval@2.0.1: {} + perfect-debounce@2.1.0: {} + picocolors@1.1.1: {} + pkg-types@2.3.1: + dependencies: + confbox: 0.2.4 + exsolve: 1.1.0 + pathe: 2.0.3 + postcss@8.5.15: dependencies: nanoid: 3.3.15 picocolors: 1.1.1 source-map-js: 1.2.1 + postgres@3.4.7: {} + + prisma@7.8.0(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7)(typescript@5.9.3): + dependencies: + '@prisma/config': 7.8.0 + '@prisma/dev': 0.24.3(typescript@5.9.3) + '@prisma/engines': 7.8.0 + '@prisma/studio-core': 0.27.3(@types/react@19.2.17)(react-dom@19.2.7(react@19.2.7))(react@19.2.7) + mysql2: 3.15.3 + postgres: 3.4.7 + optionalDependencies: + typescript: 5.9.3 + transitivePeerDependencies: + - '@types/react' + - '@types/react-dom' + - magicast + - react + - react-dom + + proper-lockfile@4.1.2: + dependencies: + graceful-fs: 4.2.11 + retry: 0.12.0 + signal-exit: 3.0.7 + + pure-rand@6.1.0: {} + + rc9@3.0.1: + dependencies: + defu: 6.1.7 + destr: 2.0.5 + + react-dom@19.2.7(react@19.2.7): + dependencies: + react: 19.2.7 + scheduler: 0.27.0 + + react@19.2.7: {} + + readdirp@5.0.0: {} + + remeda@2.33.4: {} + + require-from-string@2.0.2: {} + resolve-pkg-maps@1.0.0: {} + retry@0.12.0: {} + rollup@4.62.2: dependencies: '@types/estree': 1.0.9 @@ -1947,12 +2673,26 @@ snapshots: safer-buffer@2.1.2: {} + scheduler@0.27.0: {} + semver@7.8.5: {} + seq-queue@0.0.5: {} + + shebang-command@2.0.0: + dependencies: + shebang-regex: 3.0.0 + + shebang-regex@3.0.0: {} + shell-quote@1.9.0: {} siginfo@2.0.0: {} + signal-exit@3.0.7: {} + + signal-exit@4.1.0: {} + source-map-js@1.2.1: {} source-map-support@0.5.21: @@ -1964,6 +2704,8 @@ snapshots: sql-escaper@1.3.3: {} + sqlstring@2.3.3: {} + stackback@0.0.2: {} std-env@3.10.0: {} @@ -1984,19 +2726,16 @@ snapshots: optionalDependencies: fsevents: 2.3.3 - turbo@2.10.0: - optionalDependencies: - '@turbo/darwin-64': 2.10.0 - '@turbo/darwin-arm64': 2.10.0 - '@turbo/linux-64': 2.10.0 - '@turbo/linux-arm64': 2.10.0 - '@turbo/windows-64': 2.10.0 - '@turbo/windows-arm64': 2.10.0 - typescript@5.9.3: {} undici-types@6.21.0: {} + undici-types@7.18.2: {} + + valibot@1.2.0(typescript@5.9.3): + optionalDependencies: + typescript: 5.9.3 + vite-node@2.1.9(@types/node@22.20.0): dependencies: cac: 6.7.14 @@ -2059,6 +2798,10 @@ snapshots: - supports-color - terser + which@2.0.2: + dependencies: + isexe: 2.0.0 + which@4.0.0: dependencies: isexe: 3.1.5 @@ -2067,3 +2810,10 @@ snapshots: dependencies: siginfo: 2.0.0 stackback: 0.0.2 + + zeptomatch@2.1.0: + dependencies: + grammex: 3.1.12 + graphmatch: 1.1.1 + + zod@3.25.76: {} diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml deleted file mode 100644 index 0e5a0737..00000000 --- a/pnpm-workspace.yaml +++ /dev/null @@ -1,3 +0,0 @@ -packages: - - "packages/*" - - "apps/*" diff --git a/prisma.config.ts b/prisma.config.ts new file mode 100644 index 00000000..56b385e8 --- /dev/null +++ b/prisma.config.ts @@ -0,0 +1,16 @@ +import "dotenv/config"; +import { defineConfig, env } from "prisma/config"; + +// Prisma 7 config. The datasource URL lives here (not in schema.prisma). +// We NEVER run `prisma migrate`/`db push` against this database — it is shared +// live with the Arcturus emulator. CMS-only schema changes go in +// prisma/migrations/*.sql (idempotent) applied via `pnpm db:migrate`. +export default defineConfig({ + schema: "prisma/schema.prisma", + migrations: { + path: "prisma/migrations", + }, + datasource: { + url: env("DATABASE_URL"), + }, +}); diff --git a/prisma/migrations/0001_create_website_settings.sql b/prisma/migrations/0001_create_website_settings.sql new file mode 100644 index 00000000..46b12dc0 --- /dev/null +++ b/prisma/migrations/0001_create_website_settings.sql @@ -0,0 +1,10 @@ +-- CMS-owned settings table. Idempotent: safe to re-run. +-- Mirrors AtomCMS's Laravel `website_settings` (key UNIQUE, value TEXT). +CREATE TABLE IF NOT EXISTS website_settings ( + id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT, + `key` VARCHAR(255) NOT NULL, + value TEXT NOT NULL, + comment VARCHAR(255) NULL, + PRIMARY KEY (id), + UNIQUE KEY website_settings_key_unique (`key`) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci; diff --git a/prisma/schema.prisma b/prisma/schema.prisma new file mode 100644 index 00000000..ea9d678d --- /dev/null +++ b/prisma/schema.prisma @@ -0,0 +1,102 @@ +// AtomCMS → Next.js conversion data layer. +// +// The database schema is OWNED by the Arcturus Morningstar emulator and shared +// live with the game server. This app conforms to that schema — it never +// generates or migrates the emulator tables. CMS-only tables (website_*) are +// created via idempotent SQL in prisma/migrations/*.sql (see apply-migrations). +// +// This is the first slice (users / users_currency / bans / website_settings); +// the full table set will be regenerated via `prisma db pull` once a live DB +// connection is available. + +generator client { + provider = "prisma-client" + output = "../src/generated/prisma" +} + +datasource db { + provider = "mysql" +} + +enum users_gender { + M + F +} + +enum bans_type { + account + ip + machine + super +} + +/// Emulator-owned core account table (users). Column names mirror default.sql. +model User { + id Int @id @default(autoincrement()) + username String @unique(map: "username") @db.VarChar(25) + realName String @default("KREWS DEV") @map("real_name") @db.VarChar(25) + password String @db.VarChar(64) + mail String? @db.VarChar(500) + mailVerified String @default("0") @map("mail_verified") + accountCreated Int @map("account_created") + accountDayOfBirth Int @default(0) @map("account_day_of_birth") + lastLogin Int @default(0) @map("last_login") + lastOnline Int @default(0) @map("last_online") + motto String @default("") @db.VarChar(127) + look String @default("") @db.VarChar(256) + gender users_gender @default(M) + rank Int @default(1) + credits Int @default(2500) + pixels Int @default(500) + points Int @default(10) + online String @default("0") + // Ephemeral SSO ticket rotated on each emulator handshake ({HOTEL_NAME}-{uuid}). + // The emulator validates it byte-for-byte when the Nitro/Flash client connects. + authTicket String @default("") @map("auth_ticket") @db.VarChar(256) + ipRegister String @map("ip_register") @db.VarChar(45) + ipCurrent String @map("ip_current") @db.VarChar(45) + machineId String @default("") @map("machine_id") @db.VarChar(64) + homeRoom Int @default(0) @map("home_room") + secretKey String? @map("secret_key") @db.VarChar(40) + pincode String? @db.VarChar(11) + extraRank Int? @map("extra_rank") + + @@map("users") +} + +/// Emulator-owned multi-currency ledger. Composite PK (user_id, type), no id. +/// Currency types are fixed: Credits=-1, Duckets=0, Diamonds=5, Points=101. +model UsersCurrency { + userId Int @map("user_id") + type Int + amount Int @default(0) + + @@id([userId, type]) + @@map("users_currency") +} + +/// Emulator-owned bans table. Active bans are those with ban_expire > now(). +model Ban { + id Int @id @default(autoincrement()) + userId Int @map("user_id") + ip String @default("") @db.VarChar(50) + machineId String @default("") @map("machine_id") @db.VarChar(255) + userStaffId Int @map("user_staff_id") + timestamp Int + banExpire Int @default(0) @map("ban_expire") + banReason String @default("") @map("ban_reason") @db.VarChar(200) + type bans_type @default(account) + cfhTopic Int @default(-1) @map("cfh_topic") + + @@map("bans") +} + +/// CMS-owned key/value settings (created by prisma/migrations, not the emulator). +model WebsiteSetting { + id BigInt @id @default(autoincrement()) @db.UnsignedBigInt + key String @unique(map: "website_settings_key_unique") @db.VarChar(255) + value String @db.Text + comment String? @db.VarChar(255) + + @@map("website_settings") +} diff --git a/scripts/apply-migrations.ts b/scripts/apply-migrations.ts new file mode 100644 index 00000000..50956647 --- /dev/null +++ b/scripts/apply-migrations.ts @@ -0,0 +1,150 @@ +/** + * DB migration runner for the manual SQL files in prisma/migrations/. + * + * Why a custom runner: the database is shared with the Arcturus emulator, so we + * apply CMS-only schema changes via hand-written idempotent SQL (every file is + * `IF NOT EXISTS`-guarded) instead of `prisma migrate dev`, whose auto-diff + * would try to DROP emulator-owned tables. An `_applied_migrations` audit table + * tracks what ran so the workflow doesn't rely on memory. + * + * pnpm db:migrate run pending migrations + * pnpm db:migrate:status print pending vs applied + * pnpm db:migrate --mark mark an already-applied file without running it + */ + +import "dotenv/config"; +import { spawnSync } from "node:child_process"; +import { createHash } from "node:crypto"; +import { readdir, readFile } from "node:fs/promises"; +import { join } from "node:path"; +import { prisma } from "@/lib/prisma"; + +const MIGRATIONS_DIR = join(process.cwd(), "prisma", "migrations"); + +async function ensureAuditTable(): Promise { + await prisma.$executeRawUnsafe(` + CREATE TABLE IF NOT EXISTS _applied_migrations ( + filename VARCHAR(255) NOT NULL, + applied_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + sha256 CHAR(64) NULL, + PRIMARY KEY (filename) + ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci + `); +} + +async function appliedSet(): Promise> { + const rows = await prisma.$queryRawUnsafe<{ filename: string }[]>( + "SELECT filename FROM _applied_migrations", + ); + return new Set(rows.map((r) => r.filename)); +} + +async function listMigrations(): Promise { + const files = await readdir(MIGRATIONS_DIR); + return files.filter((f) => f.endsWith(".sql")).sort(); +} + +async function sha256(filename: string): Promise { + const content = await readFile(join(MIGRATIONS_DIR, filename)); + return createHash("sha256").update(content).digest("hex"); +} + +function runPrismaDbExecute(filename: string): boolean { + const file = join(MIGRATIONS_DIR, filename); + // shell:true so `npx` resolves to npx.cmd on Windows (direct .cmd spawn is + // blocked there since CVE-2024-27980). Quote the path for spaces. + const result = spawnSync("npx", ["prisma", "db", "execute", "--file", `"${file}"`], { + stdio: "inherit", + env: process.env, + shell: true, + }); + return result.status === 0; +} + +async function recordApplied(filename: string): Promise { + const hash = await sha256(filename); + await prisma.$executeRawUnsafe( + "INSERT INTO _applied_migrations (filename, sha256) VALUES (?, ?) " + + "ON DUPLICATE KEY UPDATE applied_at = CURRENT_TIMESTAMP(3), sha256 = VALUES(sha256)", + filename, + hash, + ); +} + +async function statusOnly(): Promise { + await ensureAuditTable(); + const applied = await appliedSet(); + const files = await listMigrations(); + if (files.length === 0) { + console.log("No migrations found in prisma/migrations/."); + return; + } + console.log("Status:"); + for (const f of files) { + const isApplied = applied.has(f); + console.log(` ${isApplied ? "OK" : " ."} ${f}${isApplied ? "" : " (PENDING)"}`); + } + const pending = files.filter((f) => !applied.has(f)); + console.log(`\n${pending.length} pending / ${files.length} total`); +} + +async function markApplied(filename: string): Promise { + await ensureAuditTable(); + const files = await listMigrations(); + if (!files.includes(filename)) { + console.error(`File not found in prisma/migrations/: ${filename}`); + process.exit(1); + } + await recordApplied(filename); + console.log(`Marked ${filename} as applied (no SQL was run).`); +} + +async function applyPending(): Promise { + await ensureAuditTable(); + const applied = await appliedSet(); + const files = await listMigrations(); + if (files.length === 0) { + console.log("No migrations found in prisma/migrations/."); + return; + } + let ran = 0; + for (const f of files) { + if (applied.has(f)) { + console.log(`OK ${f} (already applied)`); + continue; + } + console.log(`-> applying ${f} ...`); + if (!runPrismaDbExecute(f)) { + console.error(`FAILED ${f}; aborting`); + process.exit(1); + } + await recordApplied(f); + ran++; + } + console.log(ran === 0 ? "\nNothing to do — DB already in sync." : `\nApplied ${ran} migration(s).`); +} + +async function main(): Promise { + const args = process.argv.slice(2); + try { + if (args[0] === "--status") { + await statusOnly(); + } else if (args[0] === "--mark") { + const filename = args[1]; + if (!filename) { + console.error("Usage: pnpm db:migrate --mark "); + process.exit(1); + } + await markApplied(filename); + } else { + await applyPending(); + } + } finally { + await prisma.$disconnect(); + } +} + +main().catch((e) => { + console.error("FAILED:", e instanceof Error ? e.message : e); + process.exit(1); +}); diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 00000000..f0fe10f8 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,20 @@ +import { z } from "zod"; + +// Minimal validated env for the foundation. When the Next.js app is added this +// will move to @t3-oss/env-nextjs (the habbo-next pattern), but the data layer +// only needs the DB connection + a couple of values today. +const schema = z.object({ + NODE_ENV: z.enum(["development", "test", "production"]).default("development"), + DATABASE_URL: z.string().url(), + DATABASE_POOL_SIZE: z.coerce.number().int().positive().default(40), + DATABASE_IDLE_TIMEOUT_MS: z.coerce.number().int().positive().default(300_000), + HOTEL_NAME: z.string().default("Atom"), +}); + +type Env = z.infer; + +// SKIP_ENV_VALIDATION lets tooling (typecheck, tests that don't touch the DB) +// import modules transitively without a populated .env. +export const env: Env = process.env.SKIP_ENV_VALIDATION + ? (process.env as unknown as Env) + : schema.parse(process.env); diff --git a/src/lib/prisma.ts b/src/lib/prisma.ts new file mode 100644 index 00000000..2bccc7da --- /dev/null +++ b/src/lib/prisma.ts @@ -0,0 +1,27 @@ +import { PrismaMariaDb } from "@prisma/adapter-mariadb"; +import { PrismaClient } from "@/generated/prisma/client"; +import { env } from "@/env"; + +const globalForPrisma = globalThis as unknown as { prisma?: PrismaClient }; + +function createPrismaClient(): PrismaClient { + const url = new URL(env.DATABASE_URL); + const adapter = new PrismaMariaDb({ + host: url.hostname, + port: Number(url.port) || 3306, + user: decodeURIComponent(url.username), + password: decodeURIComponent(url.password), + database: url.pathname.replace(/^\//, ""), + connectionLimit: env.DATABASE_POOL_SIZE, + connectTimeout: 10_000, + idleTimeout: env.DATABASE_IDLE_TIMEOUT_MS, + }); + return new PrismaClient({ + adapter, + log: env.NODE_ENV === "development" ? ["error", "warn"] : ["error"], + }); +} + +export const prisma = globalForPrisma.prisma ?? createPrismaClient(); + +if (env.NODE_ENV !== "production") globalForPrisma.prisma = prisma; diff --git a/packages/settings/src/currency-type.test.ts b/src/lib/services/currency.test.ts similarity index 80% rename from packages/settings/src/currency-type.test.ts rename to src/lib/services/currency.test.ts index 1f6edef6..3446b211 100644 --- a/packages/settings/src/currency-type.test.ts +++ b/src/lib/services/currency.test.ts @@ -1,5 +1,5 @@ -import { describe, it, expect } from "vitest"; -import { CurrencyType, currencyLabel } from "./currency-type.js"; +import { describe, expect, it } from "vitest"; +import { CurrencyType, currencyLabel } from "./currency"; describe("CurrencyType", () => { it("uses the emulator-fixed numeric values", () => { diff --git a/packages/settings/src/currency-type.ts b/src/lib/services/currency.ts similarity index 83% rename from packages/settings/src/currency-type.ts rename to src/lib/services/currency.ts index 065174bd..a871c1d9 100644 --- a/packages/settings/src/currency-type.ts +++ b/src/lib/services/currency.ts @@ -1,4 +1,4 @@ -// Numeric values are dictated by the game emulator and MUST NOT change. +// Numeric values are dictated by the Arcturus emulator and MUST NOT change. export enum CurrencyType { Credits = -1, Duckets = 0, diff --git a/src/lib/services/site-settings.test.ts b/src/lib/services/site-settings.test.ts new file mode 100644 index 00000000..bc2cc769 --- /dev/null +++ b/src/lib/services/site-settings.test.ts @@ -0,0 +1,46 @@ +import { beforeEach, describe, expect, it, vi } from "vitest"; + +const { findMany } = vi.hoisted(() => ({ findMany: vi.fn() })); +vi.mock("@/lib/prisma", () => ({ + prisma: { websiteSetting: { findMany } }, +})); + +import { siteSettings } from "./site-settings"; + +beforeEach(() => { + findMany.mockReset(); + siteSettings.reload(); +}); + +describe("siteSettings", () => { + it("returns a value by key", async () => { + findMany.mockResolvedValue([{ key: "hotel_name", value: "AtomHotel" }]); + expect(await siteSettings.get("hotel_name")).toBe("AtomHotel"); + }); + + it("returns the fallback when key is missing", async () => { + findMany.mockResolvedValue([]); + expect(await siteSettings.get("missing", "fallback")).toBe("fallback"); + expect(await siteSettings.get("missing")).toBeNull(); + }); + + it("coerces '1'/'0' string booleans", async () => { + findMany.mockResolvedValue([ + { key: "maintenance_enabled", value: "1" }, + { key: "radio_enabled", value: "0" }, + ]); + expect(await siteSettings.getBool("maintenance_enabled")).toBe(true); + expect(await siteSettings.getBool("radio_enabled")).toBe(false); + expect(await siteSettings.getBool("missing", true)).toBe(true); + }); + + it("fetches once and caches until reload", async () => { + findMany.mockResolvedValue([{ key: "hotel_name", value: "AtomHotel" }]); + await siteSettings.get("hotel_name"); + await siteSettings.getBool("hotel_name"); + expect(findMany).toHaveBeenCalledTimes(1); + siteSettings.reload(); + await siteSettings.get("hotel_name"); + expect(findMany).toHaveBeenCalledTimes(2); + }); +}); diff --git a/src/lib/services/site-settings.ts b/src/lib/services/site-settings.ts new file mode 100644 index 00000000..52b2ffc8 --- /dev/null +++ b/src/lib/services/site-settings.ts @@ -0,0 +1,39 @@ +import { prisma } from "@/lib/prisma"; + +/** + * DB-driven CMS config, mirroring AtomCMS's `setting()` / habbo-next's + * `siteSettings`. Loads the whole website_settings table into a key→value map, + * cached in-process. Booleans are stored as the strings '1' / '0'. + */ +class SiteSettings { + private cache: Map | null = null; + + private async load(): Promise> { + if (this.cache === null) { + const rows = await prisma.websiteSetting.findMany({ + select: { key: true, value: true }, + }); + this.cache = new Map(rows.map((r) => [r.key, r.value])); + } + return this.cache; + } + + async get(key: string, fallback: string | null = null): Promise { + const map = await this.load(); + return map.has(key) ? (map.get(key) as string) : fallback; + } + + async getBool(key: string, fallback = false): Promise { + const map = await this.load(); + if (!map.has(key)) return fallback; + const v = (map.get(key) as string).toLowerCase(); + return v === "1" || v === "true"; + } + + /** Invalidate the in-process cache after a settings write. */ + reload(): void { + this.cache = null; + } +} + +export const siteSettings = new SiteSettings(); diff --git a/tsconfig.base.json b/tsconfig.json similarity index 53% rename from tsconfig.base.json rename to tsconfig.json index 35431660..2af95f05 100644 --- a/tsconfig.base.json +++ b/tsconfig.json @@ -1,16 +1,20 @@ { "compilerOptions": { "target": "ES2022", + "lib": ["ES2022"], "module": "ESNext", "moduleResolution": "Bundler", - "lib": ["ES2022"], "strict": true, "noUncheckedIndexedAccess": true, + "noEmit": true, "esModuleInterop": true, - "skipLibCheck": true, - "declaration": true, "resolveJsonModule": true, - "verbatimModuleSyntax": true, - "types": ["node"] - } + "isolatedModules": true, + "skipLibCheck": true, + "types": ["node"], + "baseUrl": ".", + "paths": { "@/*": ["./src/*"] } + }, + "include": ["src", "scripts", "prisma.config.ts", "vitest.config.ts"], + "exclude": ["node_modules"] } diff --git a/turbo.json b/turbo.json deleted file mode 100644 index c7963fd2..00000000 --- a/turbo.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "$schema": "https://turbo.build/schema.json", - "tasks": { - "build": { "dependsOn": ["^build"], "outputs": ["dist/**"] }, - "test": { "dependsOn": ["^build"] }, - "typecheck": { "dependsOn": ["^build"] } - } -} diff --git a/vitest.config.ts b/vitest.config.ts new file mode 100644 index 00000000..3372a600 --- /dev/null +++ b/vitest.config.ts @@ -0,0 +1,15 @@ +import { fileURLToPath } from "node:url"; +import { defineConfig } from "vitest/config"; + +// Local config so Vitest does not walk up into the parent Laravel repo's +// vite.config.js (this app lives inside the Laravel project tree). +export default defineConfig({ + resolve: { + alias: { + "@": fileURLToPath(new URL("./src", import.meta.url)), + }, + }, + test: { + environment: "node", + }, +}); diff --git a/vitest.workspace.ts b/vitest.workspace.ts deleted file mode 100644 index c1efc9a8..00000000 --- a/vitest.workspace.ts +++ /dev/null @@ -1 +0,0 @@ -export default ["packages/*"];