Add data import and export

This commit is contained in:
Dallas Hoffman 2023-09-24 00:57:06 -04:00
parent 7a7b40e2ba
commit 222c718509
6 changed files with 135 additions and 21 deletions

View File

@ -0,0 +1,45 @@
<script lang="ts">
import { importDb } from '$lib/db/import-db';
import Button from '../button.svelte';
import Modal from '../modal.svelte';
export let open: boolean;
async function submit(event: SubmitEvent) {
const form = event.target as HTMLFormElement;
const formData = new FormData(form);
const [dbFile] = formData.getAll('dbFile') as File[];
if (!dbFile || dbFile.name === '') {
// TODO: message that no file was selected
return;
}
const importSucceeded = await importDb(dbFile);
if (importSucceeded === true) {
form.reset();
open = false;
} else {
// TODO: message that import failed
}
}
</script>
<Modal title="Import Notes" size="sm" bind:open>
<form on:submit|preventDefault={submit}>
<p>Select a backup file that you exported previously to restore your notes.</p>
<p>
<strong>Warning:</strong>
This will overwrite all of your current notes.
</p>
<input type="file" name="dbFile" accept=".db" />
<Button type="submit">Import</Button>
</form>
</Modal>
<style lang="scss">
input {
margin: 1rem 0 1.5rem;
}
</style>

View File

@ -2,8 +2,17 @@
import Modal from '../modal.svelte'; import Modal from '../modal.svelte';
import { settings } from '$lib/stores'; import { settings } from '$lib/stores';
import SettingsColorPicker from './settings-color-picker.svelte'; import SettingsColorPicker from './settings-color-picker.svelte';
import Button from '../button.svelte';
import { exportDb } from '$lib/db/export-db';
import SettingsImportModal from './settings-import-modal.svelte';
const { open, noteScale, defaultNoteColor } = settings; const { open, noteScale, defaultNoteColor } = settings;
let importModalOpen = false;
function goToImport() {
open.set(false);
importModalOpen = true;
}
</script> </script>
<Modal title="Settings" size="sm" bind:open={$open}> <Modal title="Settings" size="sm" bind:open={$open}>
@ -22,4 +31,23 @@
<legend>Default Note Color</legend> <legend>Default Note Color</legend>
<SettingsColorPicker bind:value={$defaultNoteColor} /> <SettingsColorPicker bind:value={$defaultNoteColor} />
</fieldset> </fieldset>
<hr />
<div class="settings-buttons">
<Button on:click={exportDb}>Export Notes</Button>
<Button secondary on:click={goToImport}>Import Notes</Button>
</div>
</Modal> </Modal>
<SettingsImportModal bind:open={importModalOpen} />
<style lang="scss">
.settings-buttons {
display: flex;
flex-direction: column;
gap: 0.75rem;
margin-top: 1.75rem;
}
hr {
margin: 1.5rem 0;
}
</style>

View File

@ -1,30 +1,16 @@
import { Kysely, Migrator } from 'kysely'; import { Kysely } from 'kysely';
import { SQLocalKysely } from 'sqlocal/kysely'; import { SQLocalKysely } from 'sqlocal/kysely';
import type { Schema } from './schema'; import type { Schema } from './schema';
import { migrations } from './migrations/'; import { migrate } from './migrator';
const sqlocal = new SQLocalKysely('sticky-notes.db'); const sqlocal = new SQLocalKysely('sticky-notes.db');
const kysely = new Kysely<Schema>({ dialect: sqlocal.dialect }); const kysely = new Kysely<Schema>({ dialect: sqlocal.dialect });
await sqlocal.sql`PRAGMA foreign_keys = ON`; try {
await migrate(kysely);
const migrator = new Migrator({ } catch (err) {
db: kysely, console.error(err);
provider: {
async getMigrations() {
// TODO: Dynamic import does not work in production build
// because the imported chunk imports `sql` from the same
// chunk that imports it, so circular dependency. Vite bug.
// const { migrations } = await import('./migrations/');
return migrations;
},
},
});
const migration = await migrator.migrateToLatest();
if (migration.error) {
console.error(migration.error);
} }
export const db = kysely; export const db = kysely;
export const { getDatabaseFile, overwriteDatabaseFile } = sqlocal;

17
src/lib/db/export-db.ts Normal file
View File

@ -0,0 +1,17 @@
import { getDatabaseFile } from './client';
export async function exportDb() {
const databaseFile = await getDatabaseFile();
const fileUrl = URL.createObjectURL(databaseFile);
const now = new Date();
const timestamp = now.toISOString().split('.')[0].replace(/\:/g, '-');
const a = document.createElement('a');
a.href = fileUrl;
a.download = `notes-${timestamp}.db`;
a.click();
a.remove();
URL.revokeObjectURL(fileUrl);
}

16
src/lib/db/import-db.ts Normal file
View File

@ -0,0 +1,16 @@
import { notes, tags } from '$lib/stores';
import { db, overwriteDatabaseFile } from './client';
import { migrate } from './migrator';
export async function importDb(dbFile: File): Promise<boolean> {
try {
await overwriteDatabaseFile(dbFile);
await migrate(db);
await tags.refreshTags();
await notes.refreshNotes();
} catch (err) {
return false;
}
return true;
}

22
src/lib/db/migrator.ts Normal file
View File

@ -0,0 +1,22 @@
import { Kysely, Migrator, sql } from 'kysely';
import { migrations } from './migrations/';
import type { Schema } from './schema';
export async function migrate(db: Kysely<Schema>) {
const migrator = new Migrator({
db,
provider: {
async getMigrations() {
return migrations;
},
},
});
await sql`PRAGMA foreign_keys = ON`.execute(db);
const migration = await migrator.migrateToLatest();
if (migration.error) {
console.error(migration.error);
throw new Error('Migration failed');
}
}