chore(deps): replace @libsql with better-sqlite3

Remove @libsql/client and its transitive entries from package-lock and
add better-sqlite3 plus its TypeScript types. This updates the lockfile
to reflect switching DB client dependencies: drop the @libsql packages
and related node_modules entries, and add "better-sqlite3" and
"@types/better-sqlite3". The change reduces unused libsql artifacts and
ensures type definitions are available for the new native sqlite client.
This commit is contained in:
4gray committed 2025-12-06 09:26:27 +01:00
1 parent 72802f3f7c
commit ac39c2a9dd
3 files changed
+244 -585

No files matched your search

@@ -1,29 +1,25 @@
/**
* Database connection and initialization for Electron
* Uses Drizzle ORM with libSQL (@libsql/client)
* Default: local file under Electron userData (file: URL)
* Optional: remote instance via env (LIBSQL_URL, LIBSQL_AUTH_TOKEN)
* Uses Drizzle ORM with better-sqlite3
* Stores database file under ~/.iptvnator/databases/
*/
import { createClient } from '@libsql/client';
import type { LibSQLDatabase } from 'drizzle-orm/libsql';
import { drizzle } from 'drizzle-orm/libsql';
import Database from 'better-sqlite3';
import type { BetterSQLite3Database } from 'drizzle-orm/better-sqlite3';
import { drizzle } from 'drizzle-orm/better-sqlite3';
import { existsSync, mkdirSync } from 'fs';
import { homedir } from 'os';
import { join } from 'path';
import * as schema from './schema';
let db: LibSQLDatabase<typeof schema> | null = null;
let client: ReturnType<typeof createClient> | null = null;
let initPromise: Promise<LibSQLDatabase<typeof schema>> | null = null;
let db: BetterSQLite3Database<typeof schema> | null = null;
let sqlite: Database.Database | null = null;
let initPromise: Promise<BetterSQLite3Database<typeof schema>> | null = null;
/**
* Get the database file path
* Use a path without spaces to avoid libSQL issues
*/
function getDatabasePath(): string {
// Use home directory with a simple path (no spaces)
// This avoids "Application Support" path which has a space
const dbDir = join(homedir(), '.iptvnator', 'databases');
// Ensure the directory exists
@@ -33,37 +29,26 @@ function getDatabasePath(): string {
return join(dbDir, 'iptvnator.db');
}
/**
* Build libSQL connection options (local file by default, remote if env set)
*/
function getLibsqlConfig(): { url: string; authToken?: string } {
const remoteUrl = process.env.LIBSQL_URL?.trim();
const authToken = process.env.LIBSQL_AUTH_TOKEN?.trim();
if (remoteUrl) {
// Remote libSQL (e.g., Turso)
return { url: remoteUrl, authToken };
}
// Local file via libSQL - must use file: scheme
const filePath = getDatabasePath();
// libSQL requires file: prefix (single slash after colon for absolute paths)
return { url: `file:${filePath}` };
}
/**
* Initialize the database connection (async)
* Initialize the database connection
* Creates tables if they don't exist
*/
export async function initDatabase(): Promise<LibSQLDatabase<typeof schema>> {
export async function initDatabase(): Promise<BetterSQLite3Database<typeof schema>> {
if (db) return db;
if (initPromise) return initPromise;
initPromise = (async () => {
const cfg = getLibsqlConfig();
client = createClient(cfg);
const database = drizzle(client, { schema });
const filePath = getDatabasePath();
sqlite = new Database(filePath);
// Enable foreign keys
sqlite.pragma('foreign_keys = ON');
const database = drizzle(sqlite, { schema });
// Create tables if they don't exist
await createTables();
createTables();
db = database;
return database;
@@ -75,11 +60,11 @@ export async function initDatabase(): Promise<LibSQLDatabase<typeof schema>> {
/**
* Create tables if they don't exist
*/
async function createTables() {
if (!client) return;
function createTables() {
if (!sqlite) return;
const createTablesSQL = `
CREATE TABLE IF NOT EXISTS playlists (
const statements = [
`CREATE TABLE IF NOT EXISTS playlists (
id TEXT PRIMARY KEY,
name TEXT NOT NULL,
serverUrl TEXT,
@@ -96,18 +81,16 @@ async function createTables() {
macAddress TEXT,
url TEXT,
last_usage TEXT
);
CREATE TABLE IF NOT EXISTS categories (
)`,
`CREATE TABLE IF NOT EXISTS categories (
id INTEGER PRIMARY KEY AUTOINCREMENT,
playlist_id TEXT NOT NULL,
name TEXT NOT NULL,
type TEXT NOT NULL CHECK (type IN ('live', 'movies', 'series')),
xtream_id INTEGER NOT NULL,
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS content (
)`,
`CREATE TABLE IF NOT EXISTS content (
id INTEGER PRIMARY KEY AUTOINCREMENT,
category_id INTEGER NOT NULL,
title TEXT NOT NULL,
@@ -117,52 +100,39 @@ async function createTables() {
xtream_id INTEGER NOT NULL,
type TEXT NOT NULL CHECK (type IN ('live', 'movie', 'series')),
FOREIGN KEY (category_id) REFERENCES categories (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS recently_viewed (
)`,
`CREATE TABLE IF NOT EXISTS recently_viewed (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id INTEGER NOT NULL,
playlist_id TEXT NOT NULL,
viewed_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (content_id) REFERENCES content (id) ON DELETE CASCADE,
FOREIGN KEY (playlist_id) REFERENCES playlists (id) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS favorites (
)`,
`CREATE TABLE IF NOT EXISTS favorites (
id INTEGER PRIMARY KEY AUTOINCREMENT,
content_id INTEGER NOT NULL,
playlist_id TEXT NOT NULL,
added_at TEXT DEFAULT (datetime('now')),
FOREIGN KEY (content_id) REFERENCES content(id) ON DELETE CASCADE,
FOREIGN KEY (playlist_id) REFERENCES playlists(id) ON DELETE CASCADE
);
CREATE INDEX IF NOT EXISTS idx_content_type ON content(type);
CREATE INDEX IF NOT EXISTS idx_content_category ON content(category_id);
CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id);
CREATE INDEX IF NOT EXISTS idx_content_title ON content(title);
CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id);
CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type);
CREATE UNIQUE INDEX IF NOT EXISTS favorites_content_playlist_unique ON favorites(content_id, playlist_id);
CREATE INDEX IF NOT EXISTS favorites_playlist_idx ON favorites(playlist_id);
CREATE INDEX IF NOT EXISTS favorites_content_idx ON favorites(content_id);
CREATE UNIQUE INDEX IF NOT EXISTS recently_viewed_content_playlist_unique ON recently_viewed(content_id, playlist_id);
CREATE INDEX IF NOT EXISTS recently_viewed_playlist_idx ON recently_viewed(playlist_id);
CREATE INDEX IF NOT EXISTS recently_viewed_viewed_at_idx ON recently_viewed(viewed_at);
`;
// libSQL client doesn't guarantee multi-statement execution in one call
// Split on ';' and execute sequentially for reliability
const statements = createTablesSQL
.split(';')
.map((s) => s.trim())
.filter((s) => s.length > 0);
)`,
`CREATE INDEX IF NOT EXISTS idx_content_type ON content(type)`,
`CREATE INDEX IF NOT EXISTS idx_content_category ON content(category_id)`,
`CREATE INDEX IF NOT EXISTS idx_categories_playlist ON categories(playlist_id)`,
`CREATE INDEX IF NOT EXISTS idx_content_title ON content(title)`,
`CREATE INDEX IF NOT EXISTS idx_content_xtream ON content(xtream_id)`,
`CREATE INDEX IF NOT EXISTS idx_categories_type ON categories(type)`,
`CREATE UNIQUE INDEX IF NOT EXISTS favorites_content_playlist_unique ON favorites(content_id, playlist_id)`,
`CREATE INDEX IF NOT EXISTS favorites_playlist_idx ON favorites(playlist_id)`,
`CREATE INDEX IF NOT EXISTS favorites_content_idx ON favorites(content_id)`,
`CREATE UNIQUE INDEX IF NOT EXISTS recently_viewed_content_playlist_unique ON recently_viewed(content_id, playlist_id)`,
`CREATE INDEX IF NOT EXISTS recently_viewed_playlist_idx ON recently_viewed(playlist_id)`,
`CREATE INDEX IF NOT EXISTS recently_viewed_viewed_at_idx ON recently_viewed(viewed_at)`,
];
for (const stmt of statements) {
// Add back the semicolon when helpful for parser tolerance
await client.execute(stmt);
sqlite.exec(stmt);
}
}
@@ -170,7 +140,7 @@ async function createTables() {
* Get the database instance
* Initializes the database if not already initialized
*/
export async function getDatabase(): Promise<LibSQLDatabase<typeof schema>> {
export async function getDatabase(): Promise<BetterSQLite3Database<typeof schema>> {
if (db) return db;
return initDatabase();
}
+194 -506
View File
File diff suppressed because it is too large. Load diff
+4 -3
View File
@@ -34,7 +34,7 @@
"!**/*.map"
],
"asarUnpack": [
"node_modules/@libsql/**"
"**/better-sqlite3/**"
],
"extraMetadata": {
"main": "electron-backend/main.js"
@@ -131,7 +131,7 @@
"@angular/platform-browser": "~20.3.10",
"@angular/platform-browser-dynamic": "~20.3.10",
"@angular/router": "~20.3.10",
"@libsql/client": "0.15.15",
"better-sqlite3": "^12.5.0",
"@ngrx/component-store": "20.0.0",
"@ngrx/effects": "20.0.0",
"@ngrx/entity": "20.0.0",
@@ -149,7 +149,6 @@
"date-fns": "4.1.0",
"drizzle-orm": "0.44.6",
"electron-conf": "1.3.0",
"electron-store": "11.0.2",
"epg-parser": "0.4.0",
"fix-path": "5.0.0",
"hls.js": "1.6.13",
@@ -208,6 +207,7 @@
"@swc-node/register": "~1.9.1",
"@swc/core": "~1.5.7",
"@swc/helpers": "~0.5.11",
"@types/better-sqlite3": "^7.6.12",
"@types/jest": "^30.0.0",
"@types/lodash": "4.17.20",
"@types/mocha": "9.0.0",
@@ -223,6 +223,7 @@
"drizzle-kit": "0.31.5",
"electron": "^39.0.0",
"electron-builder": "^26.0.12",
"electron-playwright-helpers": "1.8.2",
"esbuild": "0.27.0",
"eslint": "^9.8.0",
"eslint-config-prettier": "^10.1.8",