Validate database version

This commit is contained in:
RunasSudo 2025-06-07 14:12:08 +10:00
parent 54ef0de03b
commit ecce02616c
Signed by: RunasSudo
GPG Key ID: 7234E476BF21C61A
5 changed files with 113 additions and 6 deletions

View File

@ -22,8 +22,13 @@
<div class="py-8">
<main>
<div class="mx-auto max-w-7xl px-6 lg:px-8">
<template v-if="error === null">
<NoFileView v-if="!(db.filename !== null || route.name === 'new-file')" />
<RouterView v-if="db.filename !== null || route.name === 'new-file'" />
</template>
<template v-if="error !== null">
<CriticalErrorView />
</template>
</div>
</main>
</div>
@ -31,11 +36,16 @@
</template>
<script setup lang="ts">
import { onErrorCaptured } from 'vue';
import { useRoute } from 'vue-router';
import HeaderBar from './components/HeaderBar.vue';
import { db } from './db.js';
import { error, handleCriticalError } from './error.js';
import CriticalErrorView from './pages/CriticalErrorView.vue';
import NoFileView from './pages/NoFileView.vue';
const route = useRoute();
onErrorCaptured((err) => handleCriticalError(err));
</script>

View File

@ -25,6 +25,7 @@ import { reactive } from 'vue';
import { Balance } from './amounts.ts';
import { ExtendedDatabase } from './dbutil.ts';
import { CriticalError } from './error.ts';
export const DB_VERSION = 3; // Should match schema.sql
export const DT_FORMAT = 'YYYY-MM-DD HH:mm:ss.SSS000';
@ -53,8 +54,23 @@ export const db = reactive({
}
if (filename !== null) {
// Initialise cached data
const session = await this.load();
// Validate database version
let dbVersion: {value: string}[];
try {
dbVersion = await session.select("SELECT value FROM metadata WHERE key = 'version'");
} catch (err) {
throw new CriticalError('Unable to parse database (SQL error getting metadata.version)', err);
}
if (dbVersion.length === 0) {
throw new CriticalError('Unable to parse database (no metadata.version)');
}
if (dbVersion[0].value !== DB_VERSION.toString()) {
throw new CriticalError('Unsupported database version ' + dbVersion[0].value + ' (expected ' + DB_VERSION + ')');
}
// Initialise cached data
const metadataRaw: {key: string, value: string}[] = await session.select("SELECT * FROM metadata");
const metadataObject = Object.fromEntries(metadataRaw.map((x) => [x.key, x.value]));
this.metadata.version = parseInt(metadataObject.version);
@ -62,8 +78,6 @@ export const db = reactive({
this.metadata.reporting_commodity = metadataObject.reporting_commodity;
this.metadata.dps = parseInt(metadataObject.amount_dps);
}
// TODO: Validate database version
},
load: async function(): Promise<ExtendedDatabase> {

39
src/error.ts Normal file
View File

@ -0,0 +1,39 @@
/*
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
*/
import { ref } from 'vue';
// Global error state
export const error = ref(null as CriticalError | null);
export class CriticalError extends Error {
public error?: any;
constructor(message: string, error?: any) {
super(message);
this.error = error;
}
}
export function handleCriticalError(err: any) {
if (err instanceof CriticalError) {
error.value = err;
} else {
error.value = new CriticalError('An unexpected error occurred', err);
}
}

View File

@ -25,6 +25,7 @@ import { createRouter, createWebHistory } from 'vue-router';
import App from './App.vue';
import { db } from './db.ts';
import { handleCriticalError } from './error.ts';
async function initApp() {
// Init router
@ -61,7 +62,11 @@ async function initApp() {
// Init state
const dbFilename: string = await invoke('get_open_filename');
if (dbFilename !== null) {
try {
await db.init(dbFilename); // Ensure all metadata cached before loading Vue
} catch (err) {
handleCriticalError(err);
}
}
// Create Vue app

View File

@ -0,0 +1,39 @@
<!--
DrCr: Web-based double-entry bookkeeping framework
Copyright (C) 2022-2025 Lee Yingtong Li (RunasSudo)
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <https://www.gnu.org/licenses/>.
-->
<template>
<div class="rounded-md bg-red-50 mt-4 p-4 col-span-2">
<div class="flex">
<div class="flex-shrink-0">
<XCircleIcon class="h-5 w-5 text-red-400" />
</div>
<div class="ml-3 flex-1">
<p class="text-sm text-red-700">{{ error.message }}</p>
<p class="text-sm text-red-700 mt-1" v-if="error.error">{{ error.error }}</p>
</div>
</div>
</div>
</template>
<script setup type="ts">
import { XCircleIcon } from '@heroicons/vue/24/solid';
import { error } from '../error.ts';
console.error(error.value);
</script>