sqlite local
This commit is contained in:
@@ -1,12 +1,17 @@
|
||||
import type { SQLiteDatabase } from 'expo-sqlite';
|
||||
import { DATABASE_VERSION } from './schema';
|
||||
import { DATABASE_VERSION, DEVICE_USER_ID_KEY } from './schema';
|
||||
|
||||
type Migration = {
|
||||
version: number;
|
||||
up: (db: SQLiteDatabase) => Promise<void>;
|
||||
export type MigrationDb = {
|
||||
execAsync(source: string): Promise<void>;
|
||||
getFirstAsync<T>(source: string, ...params: unknown[]): Promise<T | null>;
|
||||
withExclusiveTransactionAsync(task: (txn: MigrationDb) => Promise<void>): Promise<void>;
|
||||
};
|
||||
|
||||
const MIGRATIONS: Migration[] = [
|
||||
export type Migration = {
|
||||
version: number;
|
||||
up: (db: MigrationDb) => Promise<void>;
|
||||
};
|
||||
|
||||
export const MIGRATIONS: Migration[] = [
|
||||
{
|
||||
version: 1,
|
||||
up: async (db) => {
|
||||
@@ -52,21 +57,193 @@ CREATE INDEX IF NOT EXISTS idx_files_folder_uri ON files(folder_uri);
|
||||
`);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 3,
|
||||
up: async (db) => {
|
||||
await db.execAsync(`
|
||||
CREATE TABLE IF NOT EXISTS resource_permissions (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
resource_id TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL CHECK (resource_type IN ('folder', 'file')),
|
||||
effective_access TEXT NOT NULL CHECK (effective_access IN ('owner', 'editor', 'commenter', 'viewer')),
|
||||
inherit INTEGER NOT NULL DEFAULT 1,
|
||||
owner_id TEXT,
|
||||
shared_by_id TEXT,
|
||||
expires_at INTEGER,
|
||||
cached_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
UNIQUE (resource_id, resource_type)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS shares (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
resource_id TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL CHECK (resource_type IN ('folder', 'file')),
|
||||
recipient_type TEXT NOT NULL CHECK (recipient_type IN ('user', 'group')),
|
||||
recipient_id TEXT NOT NULL,
|
||||
relation TEXT NOT NULL CHECK (relation IN ('owner', 'editor', 'commenter', 'viewer')),
|
||||
inherit INTEGER NOT NULL DEFAULT 1,
|
||||
expires_at INTEGER,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
UNIQUE (resource_id, resource_type, recipient_type, recipient_id)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS recipients (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
recipient_type TEXT NOT NULL CHECK (recipient_type IN ('user', 'group')),
|
||||
recipient_id TEXT NOT NULL UNIQUE,
|
||||
display_name TEXT NOT NULL,
|
||||
is_active INTEGER NOT NULL DEFAULT 1,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS share_links (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
token TEXT NOT NULL UNIQUE,
|
||||
resource_id TEXT NOT NULL,
|
||||
resource_type TEXT NOT NULL CHECK (resource_type IN ('folder', 'file')),
|
||||
has_password INTEGER NOT NULL DEFAULT 0,
|
||||
allow_download INTEGER NOT NULL DEFAULT 1,
|
||||
expires_at INTEGER,
|
||||
max_downloads INTEGER,
|
||||
downloads_count INTEGER NOT NULL DEFAULT 0,
|
||||
is_revoked INTEGER NOT NULL DEFAULT 0,
|
||||
created_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL,
|
||||
CHECK (max_downloads IS NULL OR max_downloads >= 0),
|
||||
CHECK (downloads_count >= 0)
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS pending_operations (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
resource_id TEXT,
|
||||
resource_type TEXT CHECK (resource_type IN ('folder', 'file')),
|
||||
ref_type TEXT CHECK (ref_type IN ('resource', 'share', 'share_link')),
|
||||
ref_id INTEGER,
|
||||
operation TEXT NOT NULL CHECK (operation IN (
|
||||
'create_resource', 'update_metadata', 'delete_resource', 'move_resource',
|
||||
'share', 'revoke_share', 'update_share',
|
||||
'create_link', 'revoke_link'
|
||||
)),
|
||||
payload TEXT NOT NULL DEFAULT '{}',
|
||||
status TEXT NOT NULL DEFAULT 'pending' CHECK (status IN ('pending', 'in_progress', 'completed', 'failed', 'cancelled')),
|
||||
attempts INTEGER NOT NULL DEFAULT 0,
|
||||
error TEXT,
|
||||
created_at INTEGER NOT NULL,
|
||||
next_retry_at INTEGER,
|
||||
last_error_at INTEGER
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS idx_pending_queue ON pending_operations(created_at) WHERE status = 'pending';
|
||||
CREATE INDEX IF NOT EXISTS idx_pending_resource ON pending_operations(resource_id);
|
||||
`);
|
||||
await db.execAsync(
|
||||
`INSERT OR IGNORE INTO user_preferences ("key", "value", updated_at)
|
||||
VALUES ('${DEVICE_USER_ID_KEY}', lower(hex(randomblob(16))), ${Date.now()});`,
|
||||
);
|
||||
},
|
||||
},
|
||||
{
|
||||
version: 4,
|
||||
up: async (db) => {
|
||||
await db.execAsync('PRAGMA foreign_keys = OFF;');
|
||||
await db.withExclusiveTransactionAsync(async (txn) => {
|
||||
await txn.execAsync(`
|
||||
CREATE TABLE folders_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
resource_id TEXT NOT NULL UNIQUE,
|
||||
uri TEXT,
|
||||
name TEXT NOT NULL,
|
||||
"exists" INTEGER,
|
||||
parent_resource_id TEXT REFERENCES folders_new(resource_id) ON DELETE CASCADE,
|
||||
owner_id TEXT NOT NULL,
|
||||
sync_status TEXT NOT NULL DEFAULT 'local',
|
||||
added_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
await txn.execAsync(`
|
||||
INSERT INTO folders_new (id, resource_id, uri, name, "exists", parent_resource_id, owner_id, sync_status, added_at, updated_at)
|
||||
SELECT id, lower(hex(randomblob(16))), uri, name, "exists", NULL,
|
||||
(SELECT "value" FROM user_preferences WHERE "key" = '${DEVICE_USER_ID_KEY}'),
|
||||
sync_status, added_at, added_at FROM folders;
|
||||
`);
|
||||
await txn.execAsync(`
|
||||
UPDATE folders_new SET parent_resource_id = (
|
||||
SELECT parent_new.resource_id
|
||||
FROM folders old_child
|
||||
JOIN folders old_parent ON old_parent.uri = old_child.parent_uri
|
||||
JOIN folders_new parent_new ON parent_new.uri = old_parent.uri
|
||||
WHERE old_child.uri = folders_new.uri
|
||||
);
|
||||
`);
|
||||
await txn.execAsync('DROP TABLE folders;');
|
||||
await txn.execAsync('ALTER TABLE folders_new RENAME TO folders;');
|
||||
await txn.execAsync(`
|
||||
CREATE TABLE files_new (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT NOT NULL,
|
||||
resource_id TEXT NOT NULL UNIQUE,
|
||||
uri TEXT,
|
||||
name TEXT NOT NULL,
|
||||
folder_resource_id TEXT NOT NULL REFERENCES folders(resource_id) ON DELETE CASCADE,
|
||||
extension TEXT,
|
||||
size INTEGER,
|
||||
"type" TEXT,
|
||||
"exists" INTEGER,
|
||||
last_modified INTEGER,
|
||||
owner_id TEXT NOT NULL,
|
||||
sync_status TEXT NOT NULL DEFAULT 'local',
|
||||
added_at INTEGER NOT NULL,
|
||||
updated_at INTEGER NOT NULL
|
||||
);
|
||||
`);
|
||||
await txn.execAsync(`
|
||||
INSERT INTO files_new (id, resource_id, uri, name, folder_resource_id, extension, size, "type", "exists", last_modified, owner_id, sync_status, added_at, updated_at)
|
||||
SELECT f.id, lower(hex(randomblob(16))), f.uri, f.name,
|
||||
fn.resource_id,
|
||||
f.extension, f.size, f."type", f."exists", f.last_modified,
|
||||
(SELECT "value" FROM user_preferences WHERE "key" = '${DEVICE_USER_ID_KEY}'),
|
||||
f.sync_status, f.added_at, f.added_at
|
||||
FROM files f
|
||||
JOIN folders fn ON fn.uri = f.folder_uri;
|
||||
`);
|
||||
await txn.execAsync('DROP TABLE files;');
|
||||
await txn.execAsync('ALTER TABLE files_new RENAME TO files;');
|
||||
await txn.execAsync(`
|
||||
CREATE INDEX IF NOT EXISTS idx_folders_resource_id ON folders(resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_resource_id ON files(resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_files_folder_resource ON files(folder_resource_id);
|
||||
CREATE INDEX IF NOT EXISTS idx_folders_parent ON folders(parent_resource_id);
|
||||
`);
|
||||
await txn.execAsync(`PRAGMA user_version = 4;`);
|
||||
});
|
||||
await db.execAsync('PRAGMA foreign_keys = ON;');
|
||||
},
|
||||
},
|
||||
];
|
||||
|
||||
async function readUserVersion(db: SQLiteDatabase): Promise<number> {
|
||||
async function readUserVersion(db: MigrationDb): 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> {
|
||||
async function writeUserVersion(db: MigrationDb, version: number): Promise<void> {
|
||||
await db.execAsync(`PRAGMA user_version = ${version}`);
|
||||
}
|
||||
|
||||
const migrationPromises = new WeakMap<MigrationDb, Promise<void>>();
|
||||
|
||||
export async function migrateDatabase(
|
||||
db: SQLiteDatabase,
|
||||
db: MigrationDb,
|
||||
targetVersion: number = DATABASE_VERSION,
|
||||
): Promise<void> {
|
||||
const existing = migrationPromises.get(db);
|
||||
if (existing) return existing;
|
||||
|
||||
const promise = runMigrations(db, targetVersion).finally(() => {
|
||||
migrationPromises.delete(db);
|
||||
});
|
||||
migrationPromises.set(db, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function runMigrations(db: MigrationDb, targetVersion: number): Promise<void> {
|
||||
let currentVersion = await readUserVersion(db);
|
||||
|
||||
for (const migration of MIGRATIONS) {
|
||||
|
||||
Reference in New Issue
Block a user