Refactor database handling to exclusively support PostgreSQL
- Removed SQLite support from the configuration and database initialization logic. - Updated database migration scripts to focus solely on PostgreSQL migrations. - Simplified logging messages to reflect PostgreSQL usage. - Adjusted database schema definitions to remove SQLite-specific types and structures. - Modified tests to ensure compatibility with PostgreSQL, including changes to table creation and data types. - Cleaned up unused imports and code related to SQLite.
This commit is contained in:
+1
-5
@@ -52,11 +52,7 @@ AI_LLM_MODEL=free
|
|||||||
# NVIDIA Nemotron Content Safety Configuration
|
# NVIDIA Nemotron Content Safety Configuration
|
||||||
NVIDIA_NEMOTRON_API_KEY=your_nvidia_api_key_here
|
NVIDIA_NEMOTRON_API_KEY=your_nvidia_api_key_here
|
||||||
|
|
||||||
# Database Configuration
|
# Database Configuration (PostgreSQL)
|
||||||
DATABASE_TYPE=sqlite
|
|
||||||
# DATABASE_TYPE=postgres
|
|
||||||
|
|
||||||
# PostgreSQL Configuration (used when DATABASE_TYPE=postgres)
|
|
||||||
# Option 1: Use DATABASE_URL for connection string
|
# Option 1: Use DATABASE_URL for connection string
|
||||||
# DATABASE_URL=postgresql://user:password@localhost:5432/discord_bot
|
# DATABASE_URL=postgresql://user:password@localhost:5432/discord_bot
|
||||||
|
|
||||||
|
|||||||
+2
-8
@@ -1,15 +1,12 @@
|
|||||||
import { defineConfig } from "drizzle-kit";
|
import { defineConfig } from "drizzle-kit";
|
||||||
|
|
||||||
const databaseType = process.env.DATABASE_TYPE || "sqlite";
|
|
||||||
const databaseUrl = process.env.DATABASE_URL;
|
const databaseUrl = process.env.DATABASE_URL;
|
||||||
|
|
||||||
export default defineConfig({
|
export default defineConfig({
|
||||||
schema: "./src/database/schema.ts",
|
schema: "./src/database/schema.ts",
|
||||||
out: "./drizzle/migrations",
|
out: "./drizzle/migrations",
|
||||||
dialect: databaseType === "postgres" ? "postgresql" : "sqlite",
|
dialect: "postgresql",
|
||||||
dbCredentials:
|
dbCredentials: databaseUrl
|
||||||
databaseType === "postgres"
|
|
||||||
? databaseUrl
|
|
||||||
? { url: databaseUrl }
|
? { url: databaseUrl }
|
||||||
: {
|
: {
|
||||||
host: process.env.POSTGRES_HOST || "localhost",
|
host: process.env.POSTGRES_HOST || "localhost",
|
||||||
@@ -17,8 +14,5 @@ export default defineConfig({
|
|||||||
user: process.env.POSTGRES_USER || "postgres",
|
user: process.env.POSTGRES_USER || "postgres",
|
||||||
password: process.env.POSTGRES_PASSWORD || "",
|
password: process.env.POSTGRES_PASSWORD || "",
|
||||||
database: process.env.POSTGRES_DB || "moderation_bot",
|
database: process.env.POSTGRES_DB || "moderation_bot",
|
||||||
}
|
|
||||||
: {
|
|
||||||
url: "file:./.muxer-queue.db",
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -37,7 +37,6 @@
|
|||||||
"@types/pg": "^8.20.0",
|
"@types/pg": "^8.20.0",
|
||||||
"@vitejs/plugin-react": "^6.0.2",
|
"@vitejs/plugin-react": "^6.0.2",
|
||||||
"axios": "^1.16.1",
|
"axios": "^1.16.1",
|
||||||
"better-sqlite3": "^12.10.0",
|
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
"discord.js-selfbot-v13": "workspace:*",
|
"discord.js-selfbot-v13": "workspace:*",
|
||||||
"dotenv": "^17.4.2",
|
"dotenv": "^17.4.2",
|
||||||
@@ -67,7 +66,6 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@biomejs/biome": "latest",
|
"@biomejs/biome": "latest",
|
||||||
"@tailwindcss/postcss": "^4.3.0",
|
"@tailwindcss/postcss": "^4.3.0",
|
||||||
"@types/better-sqlite3": "^7.6.13",
|
|
||||||
"@types/express": "^5.0.6",
|
"@types/express": "^5.0.6",
|
||||||
"@types/node": "^25.9.0",
|
"@types/node": "^25.9.0",
|
||||||
"@types/react": "^19.2.14",
|
"@types/react": "^19.2.14",
|
||||||
|
|||||||
Generated
+13
-50
@@ -37,13 +37,10 @@ importers:
|
|||||||
version: 8.20.0
|
version: 8.20.0
|
||||||
'@vitejs/plugin-react':
|
'@vitejs/plugin-react':
|
||||||
specifier: ^6.0.2
|
specifier: ^6.0.2
|
||||||
version: 6.0.2(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0))
|
version: 6.0.2(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2))
|
||||||
axios:
|
axios:
|
||||||
specifier: ^1.16.1
|
specifier: ^1.16.1
|
||||||
version: 1.16.1
|
version: 1.16.1
|
||||||
better-sqlite3:
|
|
||||||
specifier: ^12.10.0
|
|
||||||
version: 12.10.0
|
|
||||||
clsx:
|
clsx:
|
||||||
specifier: ^2.1.1
|
specifier: ^2.1.1
|
||||||
version: 2.1.1
|
version: 2.1.1
|
||||||
@@ -55,7 +52,7 @@ importers:
|
|||||||
version: 17.4.2
|
version: 17.4.2
|
||||||
drizzle-orm:
|
drizzle-orm:
|
||||||
specifier: ^0.45.2
|
specifier: ^0.45.2
|
||||||
version: 0.45.2(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(pg@8.21.0)
|
version: 0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0)
|
||||||
express:
|
express:
|
||||||
specifier: ^5.2.1
|
specifier: ^5.2.1
|
||||||
version: 5.2.1
|
version: 5.2.1
|
||||||
@@ -109,7 +106,7 @@ importers:
|
|||||||
version: 3.6.0
|
version: 3.6.0
|
||||||
vite:
|
vite:
|
||||||
specifier: ^8.0.13
|
specifier: ^8.0.13
|
||||||
version: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0)
|
version: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)
|
||||||
winston:
|
winston:
|
||||||
specifier: ^3.19.0
|
specifier: ^3.19.0
|
||||||
version: 3.19.0
|
version: 3.19.0
|
||||||
@@ -126,9 +123,6 @@ importers:
|
|||||||
'@tailwindcss/postcss':
|
'@tailwindcss/postcss':
|
||||||
specifier: ^4.3.0
|
specifier: ^4.3.0
|
||||||
version: 4.3.0
|
version: 4.3.0
|
||||||
'@types/better-sqlite3':
|
|
||||||
specifier: ^7.6.13
|
|
||||||
version: 7.6.13
|
|
||||||
'@types/express':
|
'@types/express':
|
||||||
specifier: ^5.0.6
|
specifier: ^5.0.6
|
||||||
version: 5.0.6
|
version: 5.0.6
|
||||||
@@ -164,7 +158,7 @@ importers:
|
|||||||
version: 5.9.3
|
version: 5.9.3
|
||||||
vitest:
|
vitest:
|
||||||
specifier: latest
|
specifier: latest
|
||||||
version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0))
|
version: 4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2))
|
||||||
|
|
||||||
vendor/discord-video-stream:
|
vendor/discord-video-stream:
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -1954,9 +1948,6 @@ packages:
|
|||||||
'@tybys/wasm-util@0.10.2':
|
'@tybys/wasm-util@0.10.2':
|
||||||
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
resolution: {integrity: sha512-RoBvJ2X0wuKlWFIjrwffGw1IqZHKQqzIchKaadZZfnNpsAYp2mM0h36JtPCjNDAHGgYez/15uMBpfGwchhiMgg==}
|
||||||
|
|
||||||
'@types/better-sqlite3@7.6.13':
|
|
||||||
resolution: {integrity: sha512-NMv9ASNARoKksWtsq/SHakpYAYnhBrQgGD8zkLYk/jaK8jUGn08CfEdTRgYhMypUQAfzSP8W6gNLe0q19/t4VA==}
|
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
resolution: {integrity: sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==}
|
||||||
|
|
||||||
@@ -2239,13 +2230,6 @@ packages:
|
|||||||
before-after-hook@2.2.3:
|
before-after-hook@2.2.3:
|
||||||
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
resolution: {integrity: sha512-NzUnlZexiaH/46WDhANlyR2bXRopNg4F/zuSA3OpZnllCUgRaOF2znDioDWrmbNVsuZk6l9pMquQB38cfBZwkQ==}
|
||||||
|
|
||||||
better-sqlite3@12.10.0:
|
|
||||||
resolution: {integrity: sha512-CyzaZRQKyHkB2ZInfTTl2nvT33EbDpjkLEbE8/Zck3Ll6O0qqvuGdrJ45HgtH+HykRg88ITY3AdreBGN70aBSQ==}
|
|
||||||
engines: {node: 20.x || 22.x || 23.x || 24.x || 25.x || 26.x}
|
|
||||||
|
|
||||||
bindings@1.5.0:
|
|
||||||
resolution: {integrity: sha512-p2q/t/mhvuOj/UeLlV6566GD/guowlr0hHxClI0W9m7MWYkL1F0hLo+0Aexs9HSPCtR1SXQ0TD3MMKrXZajbiQ==}
|
|
||||||
|
|
||||||
bintrees@1.0.2:
|
bintrees@1.0.2:
|
||||||
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
resolution: {integrity: sha512-VOMgTMwjAaUG580SXn3LacVgjurrbMme7ZZNYGSSV7mmtY6QQRh0Eg3pwIcntQ77DErK1L0NxkbetjcoXzVwKw==}
|
||||||
|
|
||||||
@@ -2914,9 +2898,6 @@ packages:
|
|||||||
resolution: {integrity: sha512-B/SdeSIeRv7VlOgIjtH3dkxMI+tEy5m+OeCXfAUsirBoVoY+bGtsmvmmTFPm/G23TBY4RiTtjpcgePCfwXRjqA==}
|
resolution: {integrity: sha512-B/SdeSIeRv7VlOgIjtH3dkxMI+tEy5m+OeCXfAUsirBoVoY+bGtsmvmmTFPm/G23TBY4RiTtjpcgePCfwXRjqA==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
|
|
||||||
file-uri-to-path@1.0.0:
|
|
||||||
resolution: {integrity: sha512-0Zt+s3L7Vf1biwWZ29aARiVYLx7iMGnEUl9x33fbB/j3jR81u/O2LbqK+Bm1CDSNDKVtJ/YjwY7TUd5SkeLQLw==}
|
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
resolution: {integrity: sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==}
|
||||||
engines: {node: '>=8'}
|
engines: {node: '>=8'}
|
||||||
@@ -6288,10 +6269,6 @@ snapshots:
|
|||||||
tslib: 2.8.1
|
tslib: 2.8.1
|
||||||
optional: true
|
optional: true
|
||||||
|
|
||||||
'@types/better-sqlite3@7.6.13':
|
|
||||||
dependencies:
|
|
||||||
'@types/node': 25.8.0
|
|
||||||
|
|
||||||
'@types/body-parser@1.19.6':
|
'@types/body-parser@1.19.6':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@types/connect': 3.4.38
|
'@types/connect': 3.4.38
|
||||||
@@ -6396,10 +6373,10 @@ snapshots:
|
|||||||
dependencies:
|
dependencies:
|
||||||
'@types/node': 25.8.0
|
'@types/node': 25.8.0
|
||||||
|
|
||||||
'@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0))':
|
'@vitejs/plugin-react@6.0.2(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@rolldown/pluginutils': 1.0.1
|
'@rolldown/pluginutils': 1.0.1
|
||||||
vite: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0)
|
vite: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)
|
||||||
|
|
||||||
'@vitest/expect@4.1.7':
|
'@vitest/expect@4.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6410,13 +6387,13 @@ snapshots:
|
|||||||
chai: 6.2.2
|
chai: 6.2.2
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
|
|
||||||
'@vitest/mocker@4.1.7(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0))':
|
'@vitest/mocker@4.1.7(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2))':
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/spy': 4.1.7
|
'@vitest/spy': 4.1.7
|
||||||
estree-walker: 3.0.3
|
estree-walker: 3.0.3
|
||||||
magic-string: 0.30.21
|
magic-string: 0.30.21
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
vite: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0)
|
vite: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)
|
||||||
|
|
||||||
'@vitest/pretty-format@4.1.7':
|
'@vitest/pretty-format@4.1.7':
|
||||||
dependencies:
|
dependencies:
|
||||||
@@ -6570,15 +6547,6 @@ snapshots:
|
|||||||
|
|
||||||
before-after-hook@2.2.3: {}
|
before-after-hook@2.2.3: {}
|
||||||
|
|
||||||
better-sqlite3@12.10.0:
|
|
||||||
dependencies:
|
|
||||||
bindings: 1.5.0
|
|
||||||
prebuild-install: 7.1.3
|
|
||||||
|
|
||||||
bindings@1.5.0:
|
|
||||||
dependencies:
|
|
||||||
file-uri-to-path: 1.0.0
|
|
||||||
|
|
||||||
bintrees@1.0.2: {}
|
bintrees@1.0.2: {}
|
||||||
|
|
||||||
bl@4.1.0:
|
bl@4.1.0:
|
||||||
@@ -6929,12 +6897,10 @@ snapshots:
|
|||||||
esbuild: 0.25.12
|
esbuild: 0.25.12
|
||||||
tsx: 4.22.1
|
tsx: 4.22.1
|
||||||
|
|
||||||
drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/better-sqlite3@7.6.13)(@types/pg@8.20.0)(better-sqlite3@12.10.0)(pg@8.21.0):
|
drizzle-orm@0.45.2(@opentelemetry/api@1.9.1)(@types/pg@8.20.0)(pg@8.21.0):
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
'@types/better-sqlite3': 7.6.13
|
|
||||||
'@types/pg': 8.20.0
|
'@types/pg': 8.20.0
|
||||||
better-sqlite3: 12.10.0
|
|
||||||
pg: 8.21.0
|
pg: 8.21.0
|
||||||
|
|
||||||
dunder-proto@1.0.1:
|
dunder-proto@1.0.1:
|
||||||
@@ -7289,8 +7255,6 @@ snapshots:
|
|||||||
array-back: 4.0.2
|
array-back: 4.0.2
|
||||||
glob: 7.2.3
|
glob: 7.2.3
|
||||||
|
|
||||||
file-uri-to-path@1.0.0: {}
|
|
||||||
|
|
||||||
fill-range@7.1.1:
|
fill-range@7.1.1:
|
||||||
dependencies:
|
dependencies:
|
||||||
to-regex-range: 5.0.1
|
to-regex-range: 5.0.1
|
||||||
@@ -9088,7 +9052,7 @@ snapshots:
|
|||||||
|
|
||||||
vary@1.1.2: {}
|
vary@1.1.2: {}
|
||||||
|
|
||||||
vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0):
|
vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2):
|
||||||
dependencies:
|
dependencies:
|
||||||
lightningcss: 1.32.0
|
lightningcss: 1.32.0
|
||||||
picomatch: 4.0.4
|
picomatch: 4.0.4
|
||||||
@@ -9101,12 +9065,11 @@ snapshots:
|
|||||||
fsevents: 2.3.3
|
fsevents: 2.3.3
|
||||||
jiti: 2.7.0
|
jiti: 2.7.0
|
||||||
tsx: 4.22.2
|
tsx: 4.22.2
|
||||||
yaml: 2.9.0
|
|
||||||
|
|
||||||
vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0)):
|
vitest@4.1.7(@opentelemetry/api@1.9.1)(@types/node@25.9.0)(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)):
|
||||||
dependencies:
|
dependencies:
|
||||||
'@vitest/expect': 4.1.7
|
'@vitest/expect': 4.1.7
|
||||||
'@vitest/mocker': 4.1.7(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0))
|
'@vitest/mocker': 4.1.7(vite@8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2))
|
||||||
'@vitest/pretty-format': 4.1.7
|
'@vitest/pretty-format': 4.1.7
|
||||||
'@vitest/runner': 4.1.7
|
'@vitest/runner': 4.1.7
|
||||||
'@vitest/snapshot': 4.1.7
|
'@vitest/snapshot': 4.1.7
|
||||||
@@ -9123,7 +9086,7 @@ snapshots:
|
|||||||
tinyexec: 1.1.2
|
tinyexec: 1.1.2
|
||||||
tinyglobby: 0.2.16
|
tinyglobby: 0.2.16
|
||||||
tinyrainbow: 3.1.0
|
tinyrainbow: 3.1.0
|
||||||
vite: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)(yaml@2.9.0)
|
vite: 8.0.13(@types/node@25.9.0)(esbuild@0.28.0)(jiti@2.7.0)(tsx@4.22.2)
|
||||||
why-is-node-running: 2.3.0
|
why-is-node-running: 2.3.0
|
||||||
optionalDependencies:
|
optionalDependencies:
|
||||||
'@opentelemetry/api': 1.9.1
|
'@opentelemetry/api': 1.9.1
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export async function initializeApp() {
|
|||||||
try {
|
try {
|
||||||
logger.info("Initializing database");
|
logger.info("Initializing database");
|
||||||
await initializeDatabase();
|
await initializeDatabase();
|
||||||
logger.info({ type: config.DATABASE_TYPE }, "Database initialized");
|
logger.info("PostgreSQL database initialized");
|
||||||
} catch (err) {
|
} catch (err) {
|
||||||
logger.error({ error: err }, "Failed to initialize database");
|
logger.error({ error: err }, "Failed to initialize database");
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
|
|||||||
+1
-5
@@ -155,7 +155,6 @@ const configSchema = z
|
|||||||
.optional()
|
.optional()
|
||||||
.transform((v) => v === "true")
|
.transform((v) => v === "true")
|
||||||
.default(true),
|
.default(true),
|
||||||
DATABASE_TYPE: z.enum(["sqlite", "postgres"]).default("sqlite"),
|
|
||||||
DATABASE_URL: z.string().optional(),
|
DATABASE_URL: z.string().optional(),
|
||||||
POSTGRES_HOST: z.string().default("localhost"),
|
POSTGRES_HOST: z.string().default("localhost"),
|
||||||
POSTGRES_PORT: z.coerce.number().int().positive().default(5432),
|
POSTGRES_PORT: z.coerce.number().int().positive().default(5432),
|
||||||
@@ -179,16 +178,13 @@ const configSchema = z
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Validate PostgreSQL configuration
|
// Validate PostgreSQL configuration
|
||||||
if (value.DATABASE_TYPE === "postgres") {
|
|
||||||
if (!value.DATABASE_URL && !value.POSTGRES_HOST) {
|
if (!value.DATABASE_URL && !value.POSTGRES_HOST) {
|
||||||
ctx.addIssue({
|
ctx.addIssue({
|
||||||
code: z.ZodIssueCode.custom,
|
code: z.ZodIssueCode.custom,
|
||||||
path: ["DATABASE_URL"],
|
path: ["DATABASE_URL"],
|
||||||
message:
|
message: "Either DATABASE_URL or POSTGRES_HOST must be provided",
|
||||||
"Either DATABASE_URL or POSTGRES_HOST must be provided when DATABASE_TYPE=postgres",
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
export type AppConfig = z.infer<typeof configSchema> & {
|
export type AppConfig = z.infer<typeof configSchema> & {
|
||||||
|
|||||||
+29
-69
@@ -1,5 +1,3 @@
|
|||||||
import Database from "better-sqlite3";
|
|
||||||
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
|
||||||
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
|
import { drizzle as drizzlePostgres } from "drizzle-orm/node-postgres";
|
||||||
import { Pool } from "pg";
|
import { Pool } from "pg";
|
||||||
import { config } from "../config.js";
|
import { config } from "../config.js";
|
||||||
@@ -8,31 +6,19 @@ import * as schema from "./schema.js";
|
|||||||
|
|
||||||
const logger = createChildLogger("drizzle");
|
const logger = createChildLogger("drizzle");
|
||||||
|
|
||||||
let db:
|
let db: ReturnType<typeof drizzlePostgres> | null = null;
|
||||||
| ReturnType<typeof drizzlePostgres>
|
|
||||||
| ReturnType<typeof drizzleSqlite>
|
|
||||||
| null = null;
|
|
||||||
let rawSqlite: ReturnType<typeof Database> | null = null;
|
|
||||||
let rawPool: Pool | null = null;
|
let rawPool: Pool | null = null;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the database connection based on DATABASE_TYPE config
|
* Initialize the PostgreSQL database connection.
|
||||||
* Supports both PostgreSQL and SQLite
|
|
||||||
*/
|
*/
|
||||||
export async function initializeDatabase() {
|
export async function initializeDatabase() {
|
||||||
if (db !== null) {
|
if (db !== null) {
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
// During tests prefer an isolated SQLite instance to avoid using shared
|
|
||||||
// external Postgres instances which can lead to flaky test interference.
|
|
||||||
const usePostgres =
|
|
||||||
config.DATABASE_TYPE === "postgres" && process.env.NODE_ENV !== "test";
|
|
||||||
|
|
||||||
if (usePostgres) {
|
|
||||||
let pool: Pool;
|
let pool: Pool;
|
||||||
|
|
||||||
// Use DATABASE_URL if available, otherwise build from individual variables
|
|
||||||
if (config.DATABASE_URL) {
|
if (config.DATABASE_URL) {
|
||||||
pool = new Pool({
|
pool = new Pool({
|
||||||
connectionString: config.DATABASE_URL,
|
connectionString: config.DATABASE_URL,
|
||||||
@@ -53,35 +39,21 @@ export async function initializeDatabase() {
|
|||||||
|
|
||||||
rawPool = pool;
|
rawPool = pool;
|
||||||
db = drizzlePostgres(pool, { schema });
|
db = drizzlePostgres(pool, { schema });
|
||||||
// Provide a simple `run` helper for tests that expect it.
|
|
||||||
try {
|
try {
|
||||||
(db as any).run = (sql: string) => pool.query(sql);
|
(db as { run?: (sql: string) => Promise<unknown> }).run = (sql: string) =>
|
||||||
|
pool.query(sql);
|
||||||
} catch {
|
} catch {
|
||||||
// ignore
|
// ignore
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("PostgreSQL database initialized");
|
logger.info("PostgreSQL database initialized");
|
||||||
} else {
|
|
||||||
const sqlite = new Database(".muxer-queue.db");
|
|
||||||
sqlite.pragma("journal_mode = WAL");
|
|
||||||
|
|
||||||
rawSqlite = sqlite;
|
|
||||||
db = drizzleSqlite(sqlite, { schema });
|
|
||||||
// Expose a convenience `run` method used by tests that expect a simple API.
|
|
||||||
// `sqlite` is the underlying better-sqlite3 Database instance.
|
|
||||||
try {
|
|
||||||
(db as any).run = (sql: string) => sqlite.exec(sql);
|
|
||||||
} catch {
|
|
||||||
// ignore
|
|
||||||
}
|
|
||||||
logger.info("SQLite database initialized");
|
|
||||||
}
|
|
||||||
|
|
||||||
return db;
|
return db;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the initialized database instance
|
* Get the initialized database instance.
|
||||||
* Throws if database has not been initialized
|
* Throws if database has not been initialized.
|
||||||
*/
|
*/
|
||||||
export function getDatabase() {
|
export function getDatabase() {
|
||||||
if (db === null) {
|
if (db === null) {
|
||||||
@@ -97,51 +69,39 @@ function convertPlaceholdersForPostgres(sql: string) {
|
|||||||
return sql.replace(/\?/g, () => `$${++i}`);
|
return sql.replace(/\?/g, () => `$${++i}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeAll(sql: string, params?: any[]) {
|
export async function executeAll(sql: string, params?: unknown[]) {
|
||||||
if (rawPool) {
|
if (!rawPool) {
|
||||||
const q = convertPlaceholdersForPostgres(sql);
|
throw new Error(
|
||||||
const res = await rawPool.query(q, params || []);
|
"Database not initialized. Call initializeDatabase() first.",
|
||||||
return res.rows;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawSqlite) {
|
const query = convertPlaceholdersForPostgres(sql);
|
||||||
const stmt = rawSqlite.prepare(sql);
|
const result = await rawPool.query(query, params || []);
|
||||||
return stmt.all(...(params || []));
|
return result.rows;
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Database not initialized. Call initializeDatabase() first.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function executeGet(sql: string, params?: any[]) {
|
export async function executeGet(sql: string, params?: unknown[]) {
|
||||||
if (rawPool) {
|
if (!rawPool) {
|
||||||
const q = convertPlaceholdersForPostgres(sql);
|
throw new Error(
|
||||||
const res = await rawPool.query(q, params || []);
|
"Database not initialized. Call initializeDatabase() first.",
|
||||||
return res.rows[0] ?? null;
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
if (rawSqlite) {
|
const query = convertPlaceholdersForPostgres(sql);
|
||||||
const stmt = rawSqlite.prepare(sql);
|
const result = await rawPool.query(query, params || []);
|
||||||
return stmt.get(...(params || []));
|
return result.rows[0] ?? null;
|
||||||
}
|
|
||||||
|
|
||||||
throw new Error("Database not initialized. Call initializeDatabase() first.");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Close the database connection
|
* Close the PostgreSQL connection pool.
|
||||||
* For PostgreSQL, the pool will close on process exit
|
|
||||||
* For SQLite, closes the database connection
|
|
||||||
*/
|
*/
|
||||||
export async function closeDatabase() {
|
export async function closeDatabase() {
|
||||||
if (db === null) {
|
if (rawPool !== null) {
|
||||||
return;
|
await rawPool.end();
|
||||||
}
|
|
||||||
|
|
||||||
if (config.DATABASE_TYPE === "postgres") {
|
|
||||||
logger.info("PostgreSQL connection pool will close on process exit");
|
|
||||||
} else {
|
|
||||||
logger.info("SQLite database closed");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
rawPool = null;
|
||||||
db = null;
|
db = null;
|
||||||
|
logger.info("PostgreSQL database closed");
|
||||||
}
|
}
|
||||||
|
|||||||
+3
-24
@@ -1,45 +1,24 @@
|
|||||||
import "dotenv/config";
|
import "dotenv/config";
|
||||||
import Database from "better-sqlite3";
|
|
||||||
import { drizzle as drizzleSqlite } from "drizzle-orm/better-sqlite3";
|
|
||||||
import { migrate as migrateSqlite } from "drizzle-orm/better-sqlite3/migrator";
|
|
||||||
import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator";
|
import { migrate as migratePostgres } from "drizzle-orm/node-postgres/migrator";
|
||||||
import { config } from "../config.js";
|
|
||||||
import { createChildLogger } from "../logger.js";
|
import { createChildLogger } from "../logger.js";
|
||||||
import { closeDatabase, initializeDatabase } from "./drizzle.js";
|
import { closeDatabase, initializeDatabase } from "./drizzle.js";
|
||||||
|
|
||||||
const logger = createChildLogger("migrate");
|
const logger = createChildLogger("migrate");
|
||||||
|
|
||||||
export function initializeMigrationSqliteDatabase(path = ".muxer-queue.db") {
|
|
||||||
const sqlite = new Database(path);
|
|
||||||
sqlite.pragma("journal_mode = WAL");
|
|
||||||
return { sqlite, db: drizzleSqlite(sqlite) };
|
|
||||||
}
|
|
||||||
|
|
||||||
export async function runMigrations(): Promise<void> {
|
export async function runMigrations(): Promise<void> {
|
||||||
try {
|
try {
|
||||||
logger.info("Starting database migrations");
|
logger.info("Starting PostgreSQL migrations");
|
||||||
|
|
||||||
if (config.DATABASE_TYPE === "postgres") {
|
|
||||||
logger.info("Running PostgreSQL migrations");
|
|
||||||
const db = (await initializeDatabase()) as Parameters<
|
const db = (await initializeDatabase()) as Parameters<
|
||||||
typeof migratePostgres
|
typeof migratePostgres
|
||||||
>[0];
|
>[0];
|
||||||
|
|
||||||
try {
|
try {
|
||||||
await migratePostgres(db, { migrationsFolder: "./drizzle/migrations" });
|
await migratePostgres(db, { migrationsFolder: "./drizzle/migrations" });
|
||||||
} finally {
|
} finally {
|
||||||
await closeDatabase();
|
await closeDatabase();
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.info("PostgreSQL migrations completed successfully");
|
logger.info("PostgreSQL migrations completed successfully");
|
||||||
} else {
|
|
||||||
logger.info("Running SQLite migrations");
|
|
||||||
const { sqlite, db } = initializeMigrationSqliteDatabase();
|
|
||||||
try {
|
|
||||||
migrateSqlite(db, { migrationsFolder: "./drizzle/migrations" });
|
|
||||||
} finally {
|
|
||||||
sqlite.close();
|
|
||||||
}
|
|
||||||
logger.info("SQLite migrations completed successfully");
|
|
||||||
}
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.error(
|
logger.error(
|
||||||
{ error: error instanceof Error ? error.message : String(error) },
|
{ error: error instanceof Error ? error.message : String(error) },
|
||||||
|
|||||||
+11
-382
@@ -8,14 +8,6 @@ import {
|
|||||||
pgTable,
|
pgTable,
|
||||||
text as pgText,
|
text as pgText,
|
||||||
} from "drizzle-orm/pg-core";
|
} from "drizzle-orm/pg-core";
|
||||||
import {
|
|
||||||
index as sqliteIndex,
|
|
||||||
integer as sqliteInteger,
|
|
||||||
real as sqliteReal,
|
|
||||||
sqliteTable,
|
|
||||||
text as sqliteText,
|
|
||||||
} from "drizzle-orm/sqlite-core";
|
|
||||||
import { config } from "../config.js";
|
|
||||||
|
|
||||||
// PostgreSQL Schema
|
// PostgreSQL Schema
|
||||||
// ==================
|
// ==================
|
||||||
@@ -244,227 +236,6 @@ export const pgVoiceRecordingsTable = pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
// SQLite Schema
|
|
||||||
// =============
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Muxer Jobs Table (SQLite)
|
|
||||||
* Tracks audio post-processing jobs with status and retry logic
|
|
||||||
*/
|
|
||||||
export const sqliteMuxerJobsTable = sqliteTable(
|
|
||||||
"muxer_jobs",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
data: sqliteText("data").notNull(),
|
|
||||||
status: sqliteText("status", {
|
|
||||||
enum: ["pending", "processing", "completed", "failed"],
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default("pending"),
|
|
||||||
attempts: sqliteInteger("attempts").notNull().default(0),
|
|
||||||
maxAttempts: sqliteInteger("maxAttempts").notNull().default(3),
|
|
||||||
createdAt: sqliteInteger("createdAt").notNull(),
|
|
||||||
updatedAt: sqliteInteger("updatedAt").notNull(),
|
|
||||||
error: sqliteText("error"),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
statusIdx: sqliteIndex("idx_muxer_jobs_status").on(table.status),
|
|
||||||
createdAtIdx: sqliteIndex("idx_muxer_jobs_createdAt").on(table.createdAt),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Messages Table (SQLite)
|
|
||||||
* Stores text messages with AI moderation analysis
|
|
||||||
*/
|
|
||||||
export const sqliteMessagesTable = sqliteTable(
|
|
||||||
"messages",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
guild_id: sqliteText("guild_id").notNull(),
|
|
||||||
channel_id: sqliteText("channel_id").notNull(),
|
|
||||||
thread_id: sqliteText("thread_id"),
|
|
||||||
user_id: sqliteText("user_id").notNull(),
|
|
||||||
username: sqliteText("username").notNull(),
|
|
||||||
avatar_url: sqliteText("avatar_url"),
|
|
||||||
content: sqliteText("content").notNull(),
|
|
||||||
edited_content: sqliteText("edited_content"),
|
|
||||||
created_at: sqliteInteger("created_at").notNull(),
|
|
||||||
edited_at: sqliteInteger("edited_at"),
|
|
||||||
deleted_at: sqliteInteger("deleted_at"),
|
|
||||||
type: sqliteText("type", { enum: ["text", "edited", "deleted"] })
|
|
||||||
.notNull()
|
|
||||||
.default("text"),
|
|
||||||
metadata: sqliteText("metadata"),
|
|
||||||
ai_status: sqliteText("ai_status", {
|
|
||||||
enum: ["pending", "clean", "warn", "flagged", "error"],
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default("pending"),
|
|
||||||
ai_moderation_flags: sqliteText("ai_moderation_flags"),
|
|
||||||
ai_moderation_score: sqliteReal("ai_moderation_score"),
|
|
||||||
ai_analysis: sqliteText("ai_analysis"),
|
|
||||||
ai_categories: sqliteText("ai_categories"),
|
|
||||||
ai_severity: sqliteText("ai_severity", {
|
|
||||||
enum: ["none", "low", "medium", "high", "critical"],
|
|
||||||
}),
|
|
||||||
ai_confidence: sqliteReal("ai_confidence"),
|
|
||||||
ai_recommended_action: sqliteText("ai_recommended_action", {
|
|
||||||
enum: ["none", "monitor", "warn", "review", "delete", "escalate"],
|
|
||||||
}),
|
|
||||||
ai_analyzed_at: sqliteInteger("ai_analyzed_at"),
|
|
||||||
ai_error: sqliteText("ai_error"),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
channelIdx: sqliteIndex("idx_messages_channel").on(table.channel_id),
|
|
||||||
userIdx: sqliteIndex("idx_messages_user").on(table.user_id),
|
|
||||||
createdIdx: sqliteIndex("idx_messages_created").on(table.created_at),
|
|
||||||
threadIdx: sqliteIndex("idx_messages_thread").on(table.thread_id),
|
|
||||||
channelCreatedIdx: sqliteIndex("idx_messages_channel_created").on(
|
|
||||||
table.channel_id,
|
|
||||||
table.created_at,
|
|
||||||
table.id,
|
|
||||||
),
|
|
||||||
threadCreatedIdx: sqliteIndex("idx_messages_thread_created").on(
|
|
||||||
table.thread_id,
|
|
||||||
table.created_at,
|
|
||||||
table.id,
|
|
||||||
),
|
|
||||||
aiStatusCreatedIdx: sqliteIndex("idx_messages_ai_status_created").on(
|
|
||||||
table.ai_status,
|
|
||||||
table.created_at,
|
|
||||||
table.id,
|
|
||||||
),
|
|
||||||
guildAiStatusCreatedIdx: sqliteIndex(
|
|
||||||
"idx_messages_guild_ai_status_created",
|
|
||||||
).on(table.guild_id, table.ai_status, table.created_at, table.id),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Attachments Table (SQLite)
|
|
||||||
* Stores attachment metadata with upload status tracking
|
|
||||||
*/
|
|
||||||
export const sqliteAttachmentsTable = sqliteTable(
|
|
||||||
"attachments",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
message_id: sqliteText("message_id").notNull(),
|
|
||||||
guild_id: sqliteText("guild_id").notNull(),
|
|
||||||
channel_id: sqliteText("channel_id").notNull(),
|
|
||||||
thread_id: sqliteText("thread_id"),
|
|
||||||
user_id: sqliteText("user_id").notNull(),
|
|
||||||
filename: sqliteText("filename").notNull(),
|
|
||||||
size: sqliteInteger("size").notNull(),
|
|
||||||
type: sqliteText("type").notNull(),
|
|
||||||
discord_url: sqliteText("discord_url").notNull(),
|
|
||||||
uploaded_url: sqliteText("uploaded_url"),
|
|
||||||
upload_status: sqliteText("upload_status", {
|
|
||||||
enum: ["pending", "uploaded", "failed"],
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default("pending"),
|
|
||||||
upload_error: sqliteText("upload_error"),
|
|
||||||
created_at: sqliteInteger("created_at").notNull(),
|
|
||||||
uploaded_at: sqliteInteger("uploaded_at"),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
channelIdx: sqliteIndex("idx_attachments_channel").on(table.channel_id),
|
|
||||||
messageIdx: sqliteIndex("idx_attachments_message").on(table.message_id),
|
|
||||||
statusIdx: sqliteIndex("idx_attachments_status").on(table.upload_status),
|
|
||||||
channelCreatedIdx: sqliteIndex("idx_attachments_channel_created").on(
|
|
||||||
table.channel_id,
|
|
||||||
table.created_at,
|
|
||||||
table.id,
|
|
||||||
),
|
|
||||||
threadCreatedIdx: sqliteIndex("idx_attachments_thread_created").on(
|
|
||||||
table.thread_id,
|
|
||||||
table.created_at,
|
|
||||||
table.id,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* UI State Table (SQLite)
|
|
||||||
* Stores persistent UI state (e.g., selected channel, filter preferences)
|
|
||||||
*/
|
|
||||||
export const sqliteUIStateTable = sqliteTable("ui_state", {
|
|
||||||
key: sqliteText("key").primaryKey(),
|
|
||||||
value: sqliteText("value").notNull(),
|
|
||||||
updated_at: sqliteInteger("updated_at").notNull(),
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* AI Analysis Runs Table (SQLite)
|
|
||||||
* Tracks AI analysis batch runs for conversation-level moderation
|
|
||||||
*/
|
|
||||||
export const sqliteAIAnalysisRunsTable = sqliteTable(
|
|
||||||
"ai_analysis_runs",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
conversation_key: sqliteText("conversation_key").notNull(),
|
|
||||||
target_message_ids: sqliteText("target_message_ids").notNull(), // JSON array
|
|
||||||
model: sqliteText("model").notNull(),
|
|
||||||
request_tokens_estimate: sqliteInteger("request_tokens_estimate"),
|
|
||||||
response_raw: sqliteText("response_raw"),
|
|
||||||
status: sqliteText("status", {
|
|
||||||
enum: ["pending", "processing", "completed", "failed"],
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default("pending"),
|
|
||||||
error: sqliteText("error"),
|
|
||||||
created_at: sqliteInteger("created_at").notNull(),
|
|
||||||
completed_at: sqliteInteger("completed_at"),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
conversationKeyIdx: sqliteIndex("idx_ai_analysis_runs_conversation_key").on(
|
|
||||||
table.conversation_key,
|
|
||||||
),
|
|
||||||
statusIdx: sqliteIndex("idx_ai_analysis_runs_status").on(table.status),
|
|
||||||
createdAtIdx: sqliteIndex("idx_ai_analysis_runs_created_at").on(
|
|
||||||
table.created_at,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Voice Recordings Table (SQLite)
|
|
||||||
* Stores voice recording segment metadata and upload status
|
|
||||||
*/
|
|
||||||
export const sqliteVoiceRecordingsTable = sqliteTable(
|
|
||||||
"voice_recordings",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
user_id: sqliteText("user_id").notNull(),
|
|
||||||
username: sqliteText("username").notNull(),
|
|
||||||
avatar_url: sqliteText("avatar_url"),
|
|
||||||
guild_id: sqliteText("guild_id"),
|
|
||||||
channel_id: sqliteText("channel_id"),
|
|
||||||
channel_name: sqliteText("channel_name"),
|
|
||||||
filename: sqliteText("filename").notNull(),
|
|
||||||
size_bytes: sqliteInteger("size_bytes").notNull(),
|
|
||||||
download_url: sqliteText("download_url"),
|
|
||||||
upload_status: sqliteText("upload_status", {
|
|
||||||
enum: ["pending", "uploaded", "failed"],
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default("pending"),
|
|
||||||
upload_error: sqliteText("upload_error"),
|
|
||||||
created_at: sqliteInteger("created_at").notNull(),
|
|
||||||
uploaded_at: sqliteInteger("uploaded_at"),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
userIdIdx: sqliteIndex("idx_voice_recordings_user_id").on(table.user_id),
|
|
||||||
channelIdIdx: sqliteIndex("idx_voice_recordings_channel_id").on(
|
|
||||||
table.channel_id,
|
|
||||||
),
|
|
||||||
createdIdx: sqliteIndex("idx_voice_recordings_created_at").on(
|
|
||||||
table.created_at,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Message Reviews Table (PostgreSQL)
|
* Message Reviews Table (PostgreSQL)
|
||||||
* Tracks manual reviews of messages flagged by AI moderation
|
* Tracks manual reviews of messages flagged by AI moderation
|
||||||
@@ -502,43 +273,6 @@ export const pgMessageReviewsTable = pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Message Reviews Table (SQLite)
|
|
||||||
* Tracks manual reviews of messages flagged by AI moderation
|
|
||||||
*/
|
|
||||||
export const sqliteMessageReviewsTable = sqliteTable(
|
|
||||||
"message_reviews",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
message_id: sqliteText("message_id").notNull(),
|
|
||||||
guild_id: sqliteText("guild_id").notNull(),
|
|
||||||
channel_id: sqliteText("channel_id").notNull(),
|
|
||||||
reviewer_id: sqliteText("reviewer_id"),
|
|
||||||
status: sqliteText("status", {
|
|
||||||
enum: ["pending", "approved", "rejected", "escalated"],
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default("pending"),
|
|
||||||
notes: sqliteText("notes"),
|
|
||||||
created_at: sqliteInteger("created_at").notNull(),
|
|
||||||
reviewed_at: sqliteInteger("reviewed_at"),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
messageIdIdx: sqliteIndex("idx_message_reviews_message_id").on(
|
|
||||||
table.message_id,
|
|
||||||
),
|
|
||||||
statusIdx: sqliteIndex("idx_message_reviews_status").on(table.status),
|
|
||||||
createdAtIdx: sqliteIndex("idx_message_reviews_created_at").on(
|
|
||||||
table.created_at,
|
|
||||||
),
|
|
||||||
guildStatusIdx: sqliteIndex("idx_message_reviews_guild_status").on(
|
|
||||||
table.guild_id,
|
|
||||||
table.status,
|
|
||||||
table.created_at,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Moderation Actions Table (PostgreSQL)
|
* Moderation Actions Table (PostgreSQL)
|
||||||
* Tracks actions taken on messages (delete, mute, etc.)
|
* Tracks actions taken on messages (delete, mute, etc.)
|
||||||
@@ -584,51 +318,6 @@ export const pgModerationActionsTable = pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
|
||||||
* Moderation Actions Table (SQLite)
|
|
||||||
* Tracks actions taken on messages (delete, mute, etc.)
|
|
||||||
*/
|
|
||||||
export const sqliteModerationActionsTable = sqliteTable(
|
|
||||||
"moderation_actions",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
message_id: sqliteText("message_id"),
|
|
||||||
user_id: sqliteText("user_id"),
|
|
||||||
guild_id: sqliteText("guild_id").notNull(),
|
|
||||||
action_type: sqliteText("action_type", {
|
|
||||||
enum: [
|
|
||||||
"delete_message",
|
|
||||||
"mute_user",
|
|
||||||
"warn_user",
|
|
||||||
"kick_user",
|
|
||||||
"ban_user",
|
|
||||||
],
|
|
||||||
}).notNull(),
|
|
||||||
reason: sqliteText("reason"),
|
|
||||||
executed_by: sqliteText("executed_by"),
|
|
||||||
status: sqliteText("status", {
|
|
||||||
enum: ["pending", "executed", "failed"],
|
|
||||||
})
|
|
||||||
.notNull()
|
|
||||||
.default("pending"),
|
|
||||||
error: sqliteText("error"),
|
|
||||||
created_at: sqliteInteger("created_at").notNull(),
|
|
||||||
executed_at: sqliteInteger("executed_at"),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
messageIdIdx: sqliteIndex("idx_moderation_actions_message_id").on(
|
|
||||||
table.message_id,
|
|
||||||
),
|
|
||||||
userIdIdx: sqliteIndex("idx_moderation_actions_user_id").on(table.user_id),
|
|
||||||
statusIdx: sqliteIndex("idx_moderation_actions_status").on(table.status),
|
|
||||||
guildStatusIdx: sqliteIndex("idx_moderation_actions_guild_status").on(
|
|
||||||
table.guild_id,
|
|
||||||
table.status,
|
|
||||||
table.created_at,
|
|
||||||
),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Retention Policies Table (PostgreSQL)
|
* Retention Policies Table (PostgreSQL)
|
||||||
* Defines data retention rules per guild/channel
|
* Defines data retention rules per guild/channel
|
||||||
@@ -652,78 +341,18 @@ export const pgRetentionPoliciesTable = pgTable(
|
|||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
|
||||||
/**
|
// Runtime table exports
|
||||||
* Retention Policies Table (SQLite)
|
// =====================
|
||||||
* Defines data retention rules per guild/channel
|
|
||||||
*/
|
|
||||||
export const sqliteRetentionPoliciesTable = sqliteTable(
|
|
||||||
"retention_policies",
|
|
||||||
{
|
|
||||||
id: sqliteText("id").primaryKey(),
|
|
||||||
guild_id: sqliteText("guild_id").notNull(),
|
|
||||||
channel_id: sqliteText("channel_id"),
|
|
||||||
retention_days: sqliteInteger("retention_days").notNull().default(90),
|
|
||||||
apply_to_media: sqliteInteger("apply_to_media", { mode: "boolean" })
|
|
||||||
.notNull()
|
|
||||||
.default(true),
|
|
||||||
apply_to_voice: sqliteInteger("apply_to_voice", { mode: "boolean" })
|
|
||||||
.notNull()
|
|
||||||
.default(true),
|
|
||||||
enabled: sqliteInteger("enabled", { mode: "boolean" })
|
|
||||||
.notNull()
|
|
||||||
.default(true),
|
|
||||||
created_at: sqliteInteger("created_at").notNull(),
|
|
||||||
updated_at: sqliteInteger("updated_at").notNull(),
|
|
||||||
},
|
|
||||||
(table) => ({
|
|
||||||
guildIdIdx: sqliteIndex("idx_retention_policies_guild_id").on(
|
|
||||||
table.guild_id,
|
|
||||||
),
|
|
||||||
enabledIdx: sqliteIndex("idx_retention_policies_enabled").on(table.enabled),
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Runtime table selection based on config
|
export const muxerJobsTable = pgMuxerJobsTable;
|
||||||
// ========================================
|
export const messagesTable = pgMessagesTable;
|
||||||
|
export const attachmentsTable = pgAttachmentsTable;
|
||||||
export const muxerJobsTable =
|
export const uiStateTable = pgUIStateTable;
|
||||||
config.DATABASE_TYPE === "postgres" ? pgMuxerJobsTable : sqliteMuxerJobsTable;
|
export const aiAnalysisRunsTable = pgAIAnalysisRunsTable;
|
||||||
|
export const voiceRecordingsTable = pgVoiceRecordingsTable;
|
||||||
export const messagesTable =
|
export const messageReviewsTable = pgMessageReviewsTable;
|
||||||
config.DATABASE_TYPE === "postgres" ? pgMessagesTable : sqliteMessagesTable;
|
export const moderationActionsTable = pgModerationActionsTable;
|
||||||
|
export const retentionPoliciesTable = pgRetentionPoliciesTable;
|
||||||
export const attachmentsTable =
|
|
||||||
config.DATABASE_TYPE === "postgres"
|
|
||||||
? pgAttachmentsTable
|
|
||||||
: sqliteAttachmentsTable;
|
|
||||||
|
|
||||||
export const uiStateTable =
|
|
||||||
config.DATABASE_TYPE === "postgres" ? pgUIStateTable : sqliteUIStateTable;
|
|
||||||
|
|
||||||
export const aiAnalysisRunsTable =
|
|
||||||
config.DATABASE_TYPE === "postgres"
|
|
||||||
? pgAIAnalysisRunsTable
|
|
||||||
: sqliteAIAnalysisRunsTable;
|
|
||||||
|
|
||||||
export const voiceRecordingsTable =
|
|
||||||
config.DATABASE_TYPE === "postgres"
|
|
||||||
? pgVoiceRecordingsTable
|
|
||||||
: sqliteVoiceRecordingsTable;
|
|
||||||
|
|
||||||
export const messageReviewsTable =
|
|
||||||
config.DATABASE_TYPE === "postgres"
|
|
||||||
? pgMessageReviewsTable
|
|
||||||
: sqliteMessageReviewsTable;
|
|
||||||
|
|
||||||
export const moderationActionsTable =
|
|
||||||
config.DATABASE_TYPE === "postgres"
|
|
||||||
? pgModerationActionsTable
|
|
||||||
: sqliteModerationActionsTable;
|
|
||||||
|
|
||||||
export const retentionPoliciesTable =
|
|
||||||
config.DATABASE_TYPE === "postgres"
|
|
||||||
? pgRetentionPoliciesTable
|
|
||||||
: sqliteRetentionPoliciesTable;
|
|
||||||
|
|
||||||
// Export table types for use in queries
|
// Export table types for use in queries
|
||||||
export type MuxerJob = typeof muxerJobsTable.$inferSelect;
|
export type MuxerJob = typeof muxerJobsTable.$inferSelect;
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
import { config } from "../config.js";
|
|
||||||
import { executeAll, executeGet } from "../database/drizzle.js";
|
import { executeAll, executeGet } from "../database/drizzle.js";
|
||||||
import { createChildLogger } from "../logger.js";
|
import { createChildLogger } from "../logger.js";
|
||||||
import type { MessageRecord } from "./types.js";
|
import type { MessageRecord } from "./types.js";
|
||||||
@@ -107,11 +106,7 @@ export async function getHourlyStats(input: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const since = Date.now() - hours * 3600_000;
|
const since = Date.now() - hours * 3600_000;
|
||||||
const isPg = config.DATABASE_TYPE === "postgres";
|
const hourExpr = `to_char(to_timestamp((created_at / 3600000) * 3600), 'YYYY-MM-DD HH24:MI:SS') as hour`;
|
||||||
|
|
||||||
const hourExpr = isPg
|
|
||||||
? `to_char(to_timestamp((created_at / 3600000) * 3600), 'YYYY-MM-DD HH24:MI:SS') as hour`
|
|
||||||
: `datetime((created_at / 3600000) * 3600, 'unixepoch') as hour`;
|
|
||||||
|
|
||||||
const rows = await executeAll(
|
const rows = await executeAll(
|
||||||
`
|
`
|
||||||
@@ -531,11 +526,7 @@ export async function getModerationStats(input: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const since = Date.now() - hours * 3600_000;
|
const since = Date.now() - hours * 3600_000;
|
||||||
const isPg = config.DATABASE_TYPE === "postgres";
|
const avgScoreExpr = `round(avg(ai_moderation_score)::numeric, 2)`;
|
||||||
|
|
||||||
const avgScoreExpr = isPg
|
|
||||||
? `round(avg(ai_moderation_score)::numeric, 2)`
|
|
||||||
: `round(avg(ai_moderation_score), 2)`;
|
|
||||||
|
|
||||||
const row = await executeGet(
|
const row = await executeGet(
|
||||||
`
|
`
|
||||||
@@ -738,11 +729,7 @@ export async function getDailyTrend(input: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const since = Date.now() - hours * 3600_000;
|
const since = Date.now() - hours * 3600_000;
|
||||||
const isPg = config.DATABASE_TYPE === "postgres";
|
const dateExpr = `to_char(date_trunc('day', to_timestamp(created_at / 1000)), 'YYYY-MM-DD') as date`;
|
||||||
|
|
||||||
const dateExpr = isPg
|
|
||||||
? `to_char(date_trunc('day', to_timestamp(created_at / 1000)), 'YYYY-MM-DD') as date`
|
|
||||||
: `date(created_at / 1000, 'unixepoch') as date`;
|
|
||||||
|
|
||||||
const rows = await executeAll(
|
const rows = await executeAll(
|
||||||
`
|
`
|
||||||
@@ -832,15 +819,8 @@ export async function getActivityHeatmap(input: {
|
|||||||
|
|
||||||
try {
|
try {
|
||||||
const since = Date.now() - hours * 3600_000;
|
const since = Date.now() - hours * 3600_000;
|
||||||
const isPg = config.DATABASE_TYPE === "postgres";
|
const dayExpr = `(extract(isodow from to_timestamp(created_at / 1000)) % 7)::int as day_of_week`;
|
||||||
|
const hourExpr = `extract(hour from to_timestamp(created_at / 1000))::int as hour`;
|
||||||
// SQLite: cast to int for modulo; Postgres: use extract()
|
|
||||||
const dayExpr = isPg
|
|
||||||
? `(extract(isodow from to_timestamp(created_at / 1000)) % 7)::int as day_of_week`
|
|
||||||
: `(cast((created_at / 86400000) as integer) % 7) as day_of_week`;
|
|
||||||
const hourExpr = isPg
|
|
||||||
? `extract(hour from to_timestamp(created_at / 1000))::int as hour`
|
|
||||||
: `(cast((created_at / 3600000) as integer) % 24) as hour`;
|
|
||||||
|
|
||||||
const rows = await executeAll(
|
const rows = await executeAll(
|
||||||
`
|
`
|
||||||
|
|||||||
+1
-1
@@ -53,7 +53,7 @@ export function getDatabase() {
|
|||||||
return undefined as unknown as never;
|
return undefined as unknown as never;
|
||||||
}
|
}
|
||||||
|
|
||||||
// ── Persistent KV store (replaces SQLite uiState table) ────────────────────
|
// ── Persistent KV store ────────────────────────────────────────────────────
|
||||||
|
|
||||||
const KV_PREFIX = "kv:";
|
const KV_PREFIX = "kv:";
|
||||||
|
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import { afterAll, beforeAll, describe, expect, it, vi } from "vitest";
|
|||||||
const originalEnv = process.env;
|
const originalEnv = process.env;
|
||||||
|
|
||||||
describe("Drizzle ORM Database", () => {
|
describe("Drizzle ORM Database", () => {
|
||||||
let config: typeof import("../src/config").config;
|
|
||||||
let drizzle: typeof import("../src/database/drizzle");
|
let drizzle: typeof import("../src/database/drizzle");
|
||||||
let logger: ReturnType<typeof import("../src/logger").createChildLogger>;
|
let logger: ReturnType<typeof import("../src/logger").createChildLogger>;
|
||||||
|
|
||||||
@@ -14,22 +13,20 @@ describe("Drizzle ORM Database", () => {
|
|||||||
...originalEnv,
|
...originalEnv,
|
||||||
DISCORD_TOKEN: "test-token",
|
DISCORD_TOKEN: "test-token",
|
||||||
NODE_ENV: "test",
|
NODE_ENV: "test",
|
||||||
DATABASE_TYPE: originalEnv.DATABASE_TYPE || "sqlite",
|
|
||||||
};
|
};
|
||||||
|
|
||||||
// Reset modules to pick up new environment
|
// Reset modules to pick up new environment
|
||||||
vi.resetModules();
|
vi.resetModules();
|
||||||
|
|
||||||
// Import after environment is set
|
// Import after environment is set
|
||||||
const configModule = await import("../src/config");
|
await import("../src/config");
|
||||||
const drizzleModule = await import("../src/database/drizzle");
|
const drizzleModule = await import("../src/database/drizzle");
|
||||||
const loggerModule = await import("../src/logger");
|
const loggerModule = await import("../src/logger");
|
||||||
|
|
||||||
config = configModule.config;
|
|
||||||
drizzle = drizzleModule;
|
drizzle = drizzleModule;
|
||||||
logger = loggerModule.createChildLogger("database.test");
|
logger = loggerModule.createChildLogger("database.test");
|
||||||
|
|
||||||
logger.info(`Testing with DATABASE_TYPE: ${config.DATABASE_TYPE}`);
|
logger.info("Testing PostgreSQL database initialization");
|
||||||
});
|
});
|
||||||
|
|
||||||
afterAll(async () => {
|
afterAll(async () => {
|
||||||
|
|||||||
@@ -1,21 +1,8 @@
|
|||||||
import { mkdtempSync, rmSync } from "node:fs";
|
|
||||||
import { tmpdir } from "node:os";
|
|
||||||
import { join } from "node:path";
|
|
||||||
import { describe, expect, it } from "vitest";
|
import { describe, expect, it } from "vitest";
|
||||||
import { initializeMigrationSqliteDatabase } from "../../src/database/migrate";
|
import { runMigrations } from "../../src/database/migrate";
|
||||||
|
|
||||||
describe("initializeMigrationSqliteDatabase", () => {
|
describe("runMigrations", () => {
|
||||||
it("creates a SQLite DB with WAL journal mode", () => {
|
it("exports the PostgreSQL migration runner", () => {
|
||||||
const dir = mkdtempSync(join(tmpdir(), "bete-migrate-"));
|
expect(runMigrations).toBeTypeOf("function");
|
||||||
const dbPath = join(dir, "test.db");
|
|
||||||
const { sqlite, db } = initializeMigrationSqliteDatabase(dbPath);
|
|
||||||
|
|
||||||
try {
|
|
||||||
expect(db).toBeDefined();
|
|
||||||
expect(sqlite.pragma("journal_mode", { simple: true })).toBe("wal");
|
|
||||||
} finally {
|
|
||||||
sqlite.close();
|
|
||||||
rmSync(dir, { recursive: true, force: true });
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ type ModerationTestGlobal = typeof globalThis & {
|
|||||||
};
|
};
|
||||||
|
|
||||||
interface TestDatabase {
|
interface TestDatabase {
|
||||||
run(sql: string): void;
|
run(sql: string): Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTestDatabase(): TestDatabase {
|
function getTestDatabase(): TestDatabase {
|
||||||
@@ -63,8 +63,9 @@ function createMessage(id = "message-1"): TestMessage {
|
|||||||
|
|
||||||
async function createTables() {
|
async function createTables() {
|
||||||
const db = getTestDatabase();
|
const db = getTestDatabase();
|
||||||
db.run(`DROP TABLE IF EXISTS "messages"`);
|
await db.run(`DROP TABLE IF EXISTS "attachments" CASCADE`);
|
||||||
db.run(`
|
await db.run(`DROP TABLE IF EXISTS "messages" CASCADE`);
|
||||||
|
await db.run(`
|
||||||
CREATE TABLE IF NOT EXISTS "messages" (
|
CREATE TABLE IF NOT EXISTS "messages" (
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
"guild_id" text NOT NULL,
|
"guild_id" text NOT NULL,
|
||||||
@@ -75,9 +76,9 @@ async function createTables() {
|
|||||||
"avatar_url" text,
|
"avatar_url" text,
|
||||||
"content" text NOT NULL,
|
"content" text NOT NULL,
|
||||||
"edited_content" text,
|
"edited_content" text,
|
||||||
"created_at" integer NOT NULL,
|
"created_at" bigint NOT NULL,
|
||||||
"edited_at" integer,
|
"edited_at" bigint,
|
||||||
"deleted_at" integer,
|
"deleted_at" bigint,
|
||||||
"type" text DEFAULT 'text' NOT NULL,
|
"type" text DEFAULT 'text' NOT NULL,
|
||||||
"metadata" text,
|
"metadata" text,
|
||||||
"ai_status" text DEFAULT 'pending' NOT NULL,
|
"ai_status" text DEFAULT 'pending' NOT NULL,
|
||||||
@@ -89,11 +90,11 @@ async function createTables() {
|
|||||||
"ai_severity" text,
|
"ai_severity" text,
|
||||||
"ai_confidence" real,
|
"ai_confidence" real,
|
||||||
"ai_recommended_action" text,
|
"ai_recommended_action" text,
|
||||||
"ai_analyzed_at" integer,
|
"ai_analyzed_at" bigint,
|
||||||
"ai_error" text
|
"ai_error" text
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
db.run(`
|
await db.run(`
|
||||||
DROP TABLE IF EXISTS "attachments";
|
DROP TABLE IF EXISTS "attachments";
|
||||||
CREATE TABLE IF NOT EXISTS "attachments" (
|
CREATE TABLE IF NOT EXISTS "attachments" (
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
@@ -109,8 +110,8 @@ async function createTables() {
|
|||||||
"uploaded_url" text,
|
"uploaded_url" text,
|
||||||
"upload_status" text DEFAULT 'pending' NOT NULL,
|
"upload_status" text DEFAULT 'pending' NOT NULL,
|
||||||
"upload_error" text,
|
"upload_error" text,
|
||||||
"created_at" integer NOT NULL,
|
"created_at" bigint NOT NULL,
|
||||||
"uploaded_at" integer
|
"uploaded_at" bigint
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
}
|
}
|
||||||
@@ -124,8 +125,8 @@ describe("captureMessage", () => {
|
|||||||
beforeEach(async () => {
|
beforeEach(async () => {
|
||||||
queueMessageAnalysis.mockClear();
|
queueMessageAnalysis.mockClear();
|
||||||
const db = getTestDatabase();
|
const db = getTestDatabase();
|
||||||
db.run(`DELETE FROM "attachments"`);
|
await db.run(`DELETE FROM "attachments"`);
|
||||||
db.run(`DELETE FROM "messages"`);
|
await db.run(`DELETE FROM "messages"`);
|
||||||
delete (globalThis as ModerationTestGlobal).moderationBroadcaster;
|
delete (globalThis as ModerationTestGlobal).moderationBroadcaster;
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -19,7 +19,7 @@ import {
|
|||||||
import type { MessageRecord } from "../../src/moderation/types";
|
import type { MessageRecord } from "../../src/moderation/types";
|
||||||
|
|
||||||
interface TestDatabase {
|
interface TestDatabase {
|
||||||
run(sql: string): void;
|
run(sql: string): Promise<unknown>;
|
||||||
}
|
}
|
||||||
|
|
||||||
function getTestDatabase(): TestDatabase {
|
function getTestDatabase(): TestDatabase {
|
||||||
@@ -45,12 +45,13 @@ describe("message cursor helpers", () => {
|
|||||||
describe("message query integration tests", () => {
|
describe("message query integration tests", () => {
|
||||||
beforeAll(async () => {
|
beforeAll(async () => {
|
||||||
await initializeDatabase();
|
await initializeDatabase();
|
||||||
// Create tables using Drizzle schema (SQLite doesn't support migrations with PostgreSQL syntax)
|
// Create tables directly for isolated query integration tests
|
||||||
const db = getTestDatabase();
|
const db = getTestDatabase();
|
||||||
try {
|
try {
|
||||||
// Create messages table
|
// Create messages table
|
||||||
await db.run(`
|
await db.run(`
|
||||||
DROP TABLE IF EXISTS "messages";
|
DROP TABLE IF EXISTS "attachments" CASCADE;
|
||||||
|
DROP TABLE IF EXISTS "messages" CASCADE;
|
||||||
CREATE TABLE IF NOT EXISTS "messages" (
|
CREATE TABLE IF NOT EXISTS "messages" (
|
||||||
"id" text PRIMARY KEY NOT NULL,
|
"id" text PRIMARY KEY NOT NULL,
|
||||||
"guild_id" text NOT NULL,
|
"guild_id" text NOT NULL,
|
||||||
@@ -61,9 +62,9 @@ describe("message query integration tests", () => {
|
|||||||
"avatar_url" text,
|
"avatar_url" text,
|
||||||
"content" text NOT NULL,
|
"content" text NOT NULL,
|
||||||
"edited_content" text,
|
"edited_content" text,
|
||||||
"created_at" integer NOT NULL,
|
"created_at" bigint NOT NULL,
|
||||||
"edited_at" integer,
|
"edited_at" bigint,
|
||||||
"deleted_at" integer,
|
"deleted_at" bigint,
|
||||||
"type" text DEFAULT 'text' NOT NULL,
|
"type" text DEFAULT 'text' NOT NULL,
|
||||||
"metadata" text,
|
"metadata" text,
|
||||||
"ai_status" text DEFAULT 'pending' NOT NULL,
|
"ai_status" text DEFAULT 'pending' NOT NULL,
|
||||||
@@ -75,7 +76,7 @@ describe("message query integration tests", () => {
|
|||||||
"ai_confidence" real,
|
"ai_confidence" real,
|
||||||
"ai_recommended_action" text,
|
"ai_recommended_action" text,
|
||||||
"ai_analysis" text,
|
"ai_analysis" text,
|
||||||
"ai_analyzed_at" integer,
|
"ai_analyzed_at" bigint,
|
||||||
"ai_error" text
|
"ai_error" text
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
@@ -97,8 +98,8 @@ describe("message query integration tests", () => {
|
|||||||
"uploaded_url" text,
|
"uploaded_url" text,
|
||||||
"upload_status" text DEFAULT 'pending' NOT NULL,
|
"upload_status" text DEFAULT 'pending' NOT NULL,
|
||||||
"upload_error" text,
|
"upload_error" text,
|
||||||
"created_at" integer NOT NULL,
|
"created_at" bigint NOT NULL,
|
||||||
"uploaded_at" integer
|
"uploaded_at" bigint
|
||||||
)
|
)
|
||||||
`);
|
`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@@ -110,8 +111,8 @@ describe("message query integration tests", () => {
|
|||||||
// Clear tables before each test
|
// Clear tables before each test
|
||||||
try {
|
try {
|
||||||
const db = getTestDatabase();
|
const db = getTestDatabase();
|
||||||
await db.run(`DELETE FROM "messages"`);
|
|
||||||
await db.run(`DELETE FROM "attachments"`);
|
await db.run(`DELETE FROM "attachments"`);
|
||||||
|
await db.run(`DELETE FROM "messages"`);
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
logger.debug({ error }, "Could not clear tables");
|
logger.debug({ error }, "Could not clear tables");
|
||||||
}
|
}
|
||||||
@@ -614,6 +615,41 @@ describe("message query integration tests", () => {
|
|||||||
const msgId1 = "msg-att-1";
|
const msgId1 = "msg-att-1";
|
||||||
const msgId2 = "msg-att-2";
|
const msgId2 = "msg-att-2";
|
||||||
|
|
||||||
|
await insertMessage({
|
||||||
|
id: msgId1,
|
||||||
|
guild_id: "guild-123",
|
||||||
|
channel_id: "channel-456",
|
||||||
|
thread_id: null,
|
||||||
|
user_id: "user-789",
|
||||||
|
username: "testuser",
|
||||||
|
avatar_url: null,
|
||||||
|
content: "message with attachment 1",
|
||||||
|
edited_content: null,
|
||||||
|
created_at: Date.now(),
|
||||||
|
edited_at: null,
|
||||||
|
deleted_at: null,
|
||||||
|
type: "text",
|
||||||
|
metadata: null,
|
||||||
|
ai_status: "pending",
|
||||||
|
});
|
||||||
|
await insertMessage({
|
||||||
|
id: msgId2,
|
||||||
|
guild_id: "guild-123",
|
||||||
|
channel_id: "channel-456",
|
||||||
|
thread_id: null,
|
||||||
|
user_id: "user-789",
|
||||||
|
username: "testuser",
|
||||||
|
avatar_url: null,
|
||||||
|
content: "message with attachment 2",
|
||||||
|
edited_content: null,
|
||||||
|
created_at: Date.now(),
|
||||||
|
edited_at: null,
|
||||||
|
deleted_at: null,
|
||||||
|
type: "text",
|
||||||
|
metadata: null,
|
||||||
|
ai_status: "pending",
|
||||||
|
});
|
||||||
|
|
||||||
const attachment1 = {
|
const attachment1 = {
|
||||||
id: "att-1",
|
id: "att-1",
|
||||||
message_id: msgId1,
|
message_id: msgId1,
|
||||||
|
|||||||
Reference in New Issue
Block a user