add sqlite

This commit is contained in:
m
2026-09-09 22:50:05 +02:00
parent d839bbdf70
commit e1360c19a2
12 changed files with 329 additions and 215 deletions
+32
View File
@@ -0,0 +1,32 @@
import * as SQLite from 'expo-sqlite';
import type { SQLiteDatabase } from 'expo-sqlite';
import { migrateDatabase } from './migrations';
import { DATABASE_NAME } from './schema';
let database: SQLiteDatabase | null = null;
export async function getDatabase(): Promise<SQLiteDatabase> {
if (database) return database;
const db = await SQLite.openDatabaseAsync(DATABASE_NAME);
await migrateDatabase(db);
database = db;
return db;
}
export async function closeDatabase(): Promise<void> {
if (!database) return;
await database.closeAsync();
database = null;
}
export async function withTransaction<T>(
work: (db: SQLiteDatabase) => Promise<T>,
): Promise<T> {
const db = await getDatabase();
let result!: T;
await db.withTransactionAsync(async () => {
result = await work(db);
});
return result;
}
+4
View File
@@ -0,0 +1,4 @@
export { getDatabase, closeDatabase, withTransaction } from './client';
export * from './repositories';
export type { FolderContext, StoredFile, SyncStatus, UserPreferences } from './types';
export { DATABASE_NAME, DATABASE_VERSION } from './schema';
+79
View File
@@ -0,0 +1,79 @@
import type { SQLiteDatabase } from 'expo-sqlite';
import { DATABASE_VERSION } from './schema';
type Migration = {
version: number;
up: (db: SQLiteDatabase) => Promise<void>;
};
const MIGRATIONS: Migration[] = [
{
version: 1,
up: async (db) => {
await db.execAsync(`
PRAGMA journal_mode = 'wal';
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uri TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
"exists" INTEGER,
sync_status TEXT NOT NULL DEFAULT 'local',
added_at INTEGER NOT NULL,
parent_uri TEXT
);
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uri TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
folder_uri TEXT NOT NULL,
extension TEXT,
size INTEGER,
"type" TEXT,
"exists" INTEGER,
last_modified INTEGER,
sync_status TEXT NOT NULL DEFAULT 'local',
added_at INTEGER NOT NULL,
FOREIGN KEY (folder_uri) REFERENCES folders(uri) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_preferences (
"key" TEXT PRIMARY KEY NOT NULL,
"value" TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
},
},
{
version: 2,
up: async (db) => {
await db.execAsync(`
CREATE INDEX IF NOT EXISTS idx_files_folder_uri ON files(folder_uri);
`);
},
},
];
async function readUserVersion(db: SQLiteDatabase): Promise<number> {
const row = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
return row?.user_version ?? 0;
}
async function writeUserVersion(db: SQLiteDatabase, version: number): Promise<void> {
await db.execAsync(`PRAGMA user_version = ${version}`);
}
export async function migrateDatabase(
db: SQLiteDatabase,
targetVersion: number = DATABASE_VERSION,
): Promise<void> {
let currentVersion = await readUserVersion(db);
for (const migration of MIGRATIONS) {
if (migration.version > currentVersion && migration.version <= targetVersion) {
await migration.up(db);
await writeUserVersion(db, migration.version);
currentVersion = migration.version;
}
}
}
+60
View File
@@ -0,0 +1,60 @@
import { getDatabase } from '../client';
import { FILE_COLUMNS_SQL } from '../schema';
import type { FileEntry, FileRow, StoredFile } from '../types';
export async function saveFile(file: FileEntry, folderUri: string): Promise<void> {
const db = await getDatabase();
const exists = file.exists ? 1 : 0;
await db.runAsync(
`INSERT INTO files (uri, name, folder_uri, extension, size, "type", "exists", last_modified, added_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(uri) DO UPDATE SET
name = excluded.name, folder_uri = excluded.folder_uri, extension = excluded.extension,
size = excluded.size, "type" = excluded."type", "exists" = excluded."exists",
last_modified = excluded.last_modified`,
file.uri,
file.name,
folderUri,
file.extension ?? null,
file.size,
file.type ?? null,
exists,
file.lastModified,
Date.now(),
);
}
export async function getFiles(folderUri?: string): Promise<StoredFile[]> {
const db = await getDatabase();
const rows = folderUri
? await db.getAllAsync<FileRow>(
`SELECT ${FILE_COLUMNS_SQL} FROM files WHERE folder_uri = ? ORDER BY name ASC`,
folderUri,
)
: await db.getAllAsync<FileRow>(
`SELECT ${FILE_COLUMNS_SQL} FROM files ORDER BY name ASC`,
);
return rows.map(toStoredFile);
}
export async function removeFile(uri: string): Promise<void> {
const db = await getDatabase();
await db.runAsync('DELETE FROM files WHERE uri = ?', uri);
}
function toStoredFile(row: FileRow): StoredFile {
return {
uri: row.uri,
name: row.name,
isDirectory: false,
extension: row.extension ?? '',
exists: row.exists === 1,
size: row.size,
type: row.type ?? '',
lastModified: row.last_modified,
folderUri: row.folder_uri,
syncStatus: row.sync_status,
addedAt: row.added_at,
};
}
@@ -0,0 +1,69 @@
import { getDatabase } from '../client';
import { FOLDER_COLUMNS_SQL } from '../schema';
import type { Folder, FolderContext, FolderRow, SyncStatus } from '../types';
type SaveFolderOptions = {
parentUri?: string;
syncStatus?: SyncStatus;
};
export async function saveFolder(
folder: Folder,
options?: SaveFolderOptions,
): Promise<void> {
const db = await getDatabase();
const exists = folder.exists === undefined ? null : folder.exists ? 1 : 0;
await db.runAsync(
`INSERT INTO folders (uri, name, "exists", sync_status, added_at, parent_uri)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(uri) DO UPDATE SET
name = excluded.name, "exists" = excluded."exists", sync_status = excluded.sync_status`,
folder.uri,
folder.name,
exists,
options?.syncStatus ?? 'local',
Date.now(),
options?.parentUri ?? null,
);
}
export async function saveDirectory(folder: Folder): Promise<boolean> {
await saveFolder(folder);
return true;
}
export async function getFolders(): Promise<FolderContext[]> {
const db = await getDatabase();
const rows = await db.getAllAsync<FolderRow>(
`SELECT ${FOLDER_COLUMNS_SQL} FROM folders ORDER BY added_at ASC, name ASC`,
);
return rows.map(toFolderContext);
}
export async function getFolderFolders(parentUri: string): Promise<FolderContext[]> {
const db = await getDatabase();
const rows = await db.getAllAsync<FolderRow>(
`SELECT ${FOLDER_COLUMNS_SQL} FROM folders WHERE parent_uri = ? ORDER BY name ASC`,
parentUri,
);
return rows.map(toFolderContext);
}
export async function removeFolder(uri: string): Promise<void> {
const db = await getDatabase();
await db.runAsync('DELETE FROM folders WHERE uri = ?', uri);
}
function toFolderContext(row: FolderRow): FolderContext {
return {
syncStatus: row.sync_status,
addedAt: row.added_at,
folder: {
uri: row.uri,
name: row.name,
isDirectory: true,
exists: row.exists === null ? undefined : row.exists === 1,
},
};
}
+9
View File
@@ -0,0 +1,9 @@
export {
saveFolder,
saveDirectory,
getFolders,
getFolderFolders,
removeFolder,
} from './folders';
export { saveFile, getFiles, removeFile } from './files';
export { saveUserPreferences, getUserPreferences } from './preferences';
@@ -0,0 +1,33 @@
import { getDatabase } from '../client';
import { PREFERENCES_KEY } from '../schema';
import type { UserPreferences } from '../types';
const DEFAULT_PREFERENCES: UserPreferences = {
syncMode: 'full',
};
export async function saveUserPreferences(preferences: UserPreferences): Promise<void> {
const db = await getDatabase();
await db.runAsync(
`INSERT INTO user_preferences ("key", "value", updated_at) VALUES (?, ?, ?)
ON CONFLICT("key") DO UPDATE SET "value" = excluded."value", updated_at = excluded.updated_at`,
PREFERENCES_KEY,
JSON.stringify(preferences),
Date.now(),
);
}
export async function getUserPreferences(): Promise<UserPreferences> {
const db = await getDatabase();
const row = await db.getFirstAsync<{ value: string }>(
'SELECT "value" FROM user_preferences WHERE "key" = ?',
PREFERENCES_KEY,
);
if (!row) return DEFAULT_PREFERENCES;
try {
return { ...DEFAULT_PREFERENCES, ...JSON.parse(row.value) } satisfies UserPreferences;
} catch {
return DEFAULT_PREFERENCES;
}
}
+31
View File
@@ -0,0 +1,31 @@
export const DATABASE_NAME = 'dot.db';
export const DATABASE_VERSION = 2;
export const PREFERENCES_KEY = 'user_preferences';
export const FOLDER_COLUMNS = [
'uri',
'name',
'"exists"',
'sync_status',
'added_at',
'parent_uri',
] as const;
export const FOLDER_COLUMNS_SQL = FOLDER_COLUMNS.join(', ');
export const FILE_COLUMNS = [
'uri',
'name',
'folder_uri',
'extension',
'size',
'"type"',
'"exists"',
'last_modified',
'sync_status',
'added_at',
] as const;
export const FILE_COLUMNS_SQL = FILE_COLUMNS.join(', ');
@@ -1,4 +1,6 @@
import type { FileEntry, Folder } from "./safDirectory.types";
import type { FileEntry, Folder } from '../safDirectory.types';
export type { FileEntry, Folder } from '../safDirectory.types';
export type SyncStatus = 'local' | 'cloud' | 'local-cloud';
+2 -2
View File
@@ -9,6 +9,6 @@ export {
saveFolder,
saveDirectory,
saveUserPreferences,
} from './sqliteStorage';
} from './db';
export type { FolderContext, StoredFile, SyncStatus, UserPreferences } from './sqliteStorage.types';
export type { FolderContext, StoredFile, SyncStatus, UserPreferences } from './db';
-209
View File
@@ -1,209 +0,0 @@
import * as SQLite from 'expo-sqlite';
import type { SQLiteDatabase } from 'expo-sqlite';
import type { FileEntry, Folder } from './safDirectory.types';
import type { UserPreferences,SyncStatus,FolderContext, FolderRow,StoredFile, FileRow } from './sqliteStorage.types';
const DATABASE_NAME = 'dot.db';
const DATABASE_VERSION = 1;
const PREFERENCES_KEY = 'user_preferences';
let database: SQLiteDatabase | null = null;
const DEFAULT_PREFERENCES: UserPreferences = {
syncMode: 'full',
};
export async function getDatabase(): Promise<SQLiteDatabase> {
if (database) return database;
const db = await SQLite.openDatabaseAsync(DATABASE_NAME);
await migrateIfNeeded(db);
database = db;
return db;
}
async function migrateIfNeeded(db: SQLiteDatabase): Promise<void> {
const row = await db.getFirstAsync<{ user_version: number }>('PRAGMA user_version');
const currentVersion = row?.user_version ?? 0;
if (currentVersion >= DATABASE_VERSION) return;
if (currentVersion === 0) {
await db.execAsync(`
PRAGMA journal_mode = 'wal';
PRAGMA foreign_keys = ON;
CREATE TABLE IF NOT EXISTS folders (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uri TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
"exists" INTEGER,
sync_status TEXT NOT NULL DEFAULT 'local',
added_at INTEGER NOT NULL,
parent_uri TEXT
);
CREATE TABLE IF NOT EXISTS files (
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
uri TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
folder_uri TEXT NOT NULL,
extension TEXT,
size INTEGER,
"type" TEXT,
"exists" INTEGER,
last_modified INTEGER,
sync_status TEXT NOT NULL DEFAULT 'local',
added_at INTEGER NOT NULL,
FOREIGN KEY (folder_uri) REFERENCES folders(uri) ON DELETE CASCADE
);
CREATE TABLE IF NOT EXISTS user_preferences (
"key" TEXT PRIMARY KEY NOT NULL,
"value" TEXT NOT NULL,
updated_at INTEGER NOT NULL
);
`);
}
await db.execAsync(`PRAGMA user_version = ${DATABASE_VERSION}`);
}
export async function saveFolder(
folder: Folder,
options?: { parentUri?: string; syncStatus?: SyncStatus },
): Promise<void> {
const db = await getDatabase();
const exists = folder.exists === undefined ? null : folder.exists ? 1 : 0;
await db.runAsync(
`INSERT INTO folders (uri, name, "exists", sync_status, added_at, parent_uri)
VALUES (?, ?, ?, ?, ?, ?)
ON CONFLICT(uri) DO UPDATE SET
name = excluded.name, "exists" = excluded."exists", sync_status = excluded.sync_status`,
folder.uri,
folder.name,
exists,
options?.syncStatus ?? 'local',
Date.now(),
options?.parentUri ?? null,
);
}
export async function saveDirectory(folder: Folder): Promise<boolean> {
await saveFolder(folder);
return true;
}
export async function getFolders(): Promise<FolderContext[]> {
const db = await getDatabase();
const rows = await db.getAllAsync<FolderRow>(
'SELECT * FROM folders ORDER BY added_at ASC, name ASC',
);
return rows.map(toFolderContext);
}
export async function getFolderFolders(parentUri: string): Promise<FolderContext[]> {
const db = await getDatabase();
const rows = await db.getAllAsync<FolderRow>(
'SELECT * FROM folders WHERE parent_uri = ? ORDER BY name ASC',
parentUri,
);
return rows.map(toFolderContext);
}
export async function removeFolder(uri: string): Promise<void> {
const db = await getDatabase();
await db.runAsync('DELETE FROM folders WHERE uri = ?', uri);
}
export async function saveFile(file: FileEntry, folderUri: string): Promise<void> {
const db = await getDatabase();
const exists = file.exists ? 1 : 0;
await db.runAsync(
`INSERT INTO files (uri, name, folder_uri, extension, size, "type", "exists", last_modified, added_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
ON CONFLICT(uri) DO UPDATE SET
name = excluded.name, folder_uri = excluded.folder_uri, extension = excluded.extension,
size = excluded.size, "type" = excluded."type", "exists" = excluded."exists",
last_modified = excluded.last_modified`,
file.uri,
file.name,
folderUri,
file.extension ?? null,
file.size,
file.type ?? null,
exists,
file.lastModified,
Date.now(),
);
}
export async function getFiles(folderUri?: string): Promise<StoredFile[]> {
const db = await getDatabase();
const rows = folderUri
? await db.getAllAsync<FileRow>(
'SELECT * FROM files WHERE folder_uri = ? ORDER BY name ASC',
folderUri,
)
: await db.getAllAsync<FileRow>('SELECT * FROM files ORDER BY name ASC');
return rows.map(toStoredFile);
}
export async function removeFile(uri: string): Promise<void> {
const db = await getDatabase();
await db.runAsync('DELETE FROM files WHERE uri = ?', uri);
}
export async function saveUserPreferences(preferences: UserPreferences): Promise<void> {
const db = await getDatabase();
await db.runAsync(
`INSERT INTO user_preferences ("key", "value", updated_at) VALUES (?, ?, ?)
ON CONFLICT("key") DO UPDATE SET "value" = excluded."value", updated_at = excluded.updated_at`,
PREFERENCES_KEY,
JSON.stringify(preferences),
Date.now(),
);
}
export async function getUserPreferences(): Promise<UserPreferences> {
const db = await getDatabase();
const row = await db.getFirstAsync<{ value: string }>(
'SELECT "value" FROM user_preferences WHERE "key" = ?',
PREFERENCES_KEY,
);
if (!row) return DEFAULT_PREFERENCES;
try {
return { ...DEFAULT_PREFERENCES, ...JSON.parse(row.value) } satisfies UserPreferences;
} catch {
return DEFAULT_PREFERENCES;
}
}
function toFolderContext(row: FolderRow): FolderContext {
return {
syncStatus: row.sync_status,
addedAt: row.added_at,
folder: {
uri: row.uri,
name: row.name,
isDirectory: true,
exists: row.exists === null ? undefined : row.exists === 1,
},
};
}
function toStoredFile(row: FileRow): StoredFile {
return {
uri: row.uri,
name: row.name,
isDirectory: false,
extension: row.extension ?? '',
exists: row.exists === 1,
size: row.size,
type: row.type ?? '',
lastModified: row.last_modified,
folderUri: row.folder_uri,
syncStatus: row.sync_status,
addedAt: row.added_at,
};
}