feat: use a local database per user
This commit is contained in:
parent
508e63aca2
commit
b750dc3511
|
|
@ -18,7 +18,7 @@ export type LocalExchangeRate = {
|
|||
updatedAt: string;
|
||||
};
|
||||
|
||||
export const db = new Dexie("cents") as Dexie & {
|
||||
export type CentsDatabase = Dexie & {
|
||||
entries: EntityTable<LedgerEntry, "id">;
|
||||
syncOperations: EntityTable<SyncOperation, "id">;
|
||||
ledgers: EntityTable<LedgerRecord, "id">;
|
||||
|
|
@ -27,6 +27,7 @@ export const db = new Dexie("cents") as Dexie & {
|
|||
exchangeRates: EntityTable<LocalExchangeRate, "id">;
|
||||
};
|
||||
|
||||
function configureDatabase(db: CentsDatabase) {
|
||||
db.version(1).stores({
|
||||
entries: "id, ledgerId, occurredAt, updatedAt, deletedAt",
|
||||
syncOperations: "id, ledgerId, createdAt, syncedAt",
|
||||
|
|
@ -107,6 +108,77 @@ db.version(5).stores({
|
|||
normalizeEntry(operation.payload);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
const databaseCache = new Map<string, CentsDatabase>();
|
||||
const databaseOpeners = new Map<string, Promise<CentsDatabase>>();
|
||||
|
||||
export async function getUserDb(userId: string) {
|
||||
const name = `cents-user-${userId}`;
|
||||
const cached = databaseCache.get(name);
|
||||
if (cached) return cached;
|
||||
const opening = databaseOpeners.get(name);
|
||||
if (opening) return opening;
|
||||
const promise = (async () => {
|
||||
const database = new Dexie(name) as CentsDatabase;
|
||||
configureDatabase(database);
|
||||
await database.open();
|
||||
await migrateLegacyUserData(database, userId);
|
||||
databaseCache.set(name, database);
|
||||
return database;
|
||||
})();
|
||||
databaseOpeners.set(name, promise);
|
||||
try {
|
||||
return await promise;
|
||||
} finally {
|
||||
databaseOpeners.delete(name);
|
||||
}
|
||||
}
|
||||
|
||||
export function closeUserDb(userId: string) {
|
||||
const name = `cents-user-${userId}`;
|
||||
const database = databaseCache.get(name);
|
||||
database?.close();
|
||||
databaseCache.delete(name);
|
||||
}
|
||||
|
||||
async function migrateLegacyUserData(database: CentsDatabase, userId: string) {
|
||||
const markerId = `${userId}:legacy-migration-v1`;
|
||||
if (await database.userPreferences.get(markerId)) return;
|
||||
const legacy = new Dexie("cents") as CentsDatabase;
|
||||
configureDatabase(legacy);
|
||||
try {
|
||||
await legacy.open();
|
||||
const [entries, operations, preferences, exchangeRates] = await Promise.all([
|
||||
legacy.entries.filter((entry) => entry.ownerId === userId).toArray(),
|
||||
legacy.syncOperations.filter((operation) => (operation.userId ?? operation.payload.updatedBy) === userId).toArray(),
|
||||
legacy.userPreferences.filter((preference) => preference.userId === userId).toArray(),
|
||||
legacy.exchangeRates.toArray(),
|
||||
]);
|
||||
const operationEntries = await Promise.all(
|
||||
operations.map((operation) => legacy.entries.get(operation.entityId)),
|
||||
);
|
||||
const mergedEntries = new Map(entries.map((entry) => [entry.id, entry]));
|
||||
for (const entry of operationEntries) {
|
||||
if (entry) mergedEntries.set(entry.id, entry);
|
||||
}
|
||||
await database.transaction("rw", database.entries, database.syncOperations, database.userPreferences, database.exchangeRates, async () => {
|
||||
if (mergedEntries.size) await database.entries.bulkPut([...mergedEntries.values()]);
|
||||
if (operations.length) await database.syncOperations.bulkPut(operations);
|
||||
if (preferences.length) await database.userPreferences.bulkPut(preferences);
|
||||
if (exchangeRates.length) await database.exchangeRates.bulkPut(exchangeRates);
|
||||
await database.userPreferences.put({
|
||||
id: markerId,
|
||||
userId,
|
||||
key: "legacyMigration",
|
||||
value: "v1",
|
||||
updatedAt: new Date().toISOString(),
|
||||
});
|
||||
});
|
||||
} finally {
|
||||
legacy.close();
|
||||
}
|
||||
}
|
||||
|
||||
function localDate(value: string) {
|
||||
const date = new Date(value);
|
||||
|
|
|
|||
|
|
@ -41,7 +41,7 @@ export type LedgerRecord = {
|
|||
export type UserPreference = {
|
||||
id: string;
|
||||
userId: string;
|
||||
key: "currentLedgerId" | "ledgerAmountDisplay";
|
||||
key: "currentLedgerId" | "ledgerAmountDisplay" | "legacyMigration";
|
||||
value: string;
|
||||
updatedAt: string;
|
||||
};
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { apiRequest } from "../data/api";
|
||||
import { closeUserDb } from "../data/db";
|
||||
|
||||
export type SessionUser = { id: string; name: string };
|
||||
|
||||
|
|
@ -50,7 +51,9 @@ export const useAuthStore = defineStore("auth", {
|
|||
this.unavailable = false;
|
||||
},
|
||||
async logout() {
|
||||
const userId = this.user?.id;
|
||||
await apiRequest("/api/auth/logout", { method: "POST" });
|
||||
if (userId) closeUserDb(userId);
|
||||
this.user = null;
|
||||
this.loaded = true;
|
||||
},
|
||||
|
|
|
|||
|
|
@ -1,7 +1,7 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { calculateCnyAmount, type CurrencyCode, type LedgerEntry, type SyncOperation } from "@cents/domain";
|
||||
import { apiRequest } from "../data/api";
|
||||
import { db } from "../data/db";
|
||||
import { getUserDb } from "../data/db";
|
||||
import { localDate } from "../data/entries";
|
||||
import { createId } from "../data/ids";
|
||||
import { useAuthStore } from "./auth";
|
||||
|
|
@ -76,6 +76,7 @@ export const useEntryStore = defineStore("entries", {
|
|||
this.loadedForUserId = "";
|
||||
return;
|
||||
}
|
||||
const db = await getUserDb(auth.user.id);
|
||||
const allowedLedgerIds = new Set(useLedgerStore().ledgers.map((ledger) => ledger.id));
|
||||
const canAccessEntry = (entry: LedgerEntry) =>
|
||||
entry.ownerId === auth.user!.id || entry.ledgerIds.some((ledgerId) => allowedLedgerIds.has(ledgerId));
|
||||
|
|
@ -113,6 +114,7 @@ export const useEntryStore = defineStore("entries", {
|
|||
return;
|
||||
}
|
||||
const userId = auth.user.id;
|
||||
const db = await getUserDb(userId);
|
||||
if (syncPromise) return syncPromise;
|
||||
|
||||
syncPromise = (async () => {
|
||||
|
|
@ -184,6 +186,7 @@ export const useEntryStore = defineStore("entries", {
|
|||
},
|
||||
async addEntry(entry: LedgerEntry) {
|
||||
const auth = useAuthStore();
|
||||
const db = await getUserDb(auth.user?.id ?? entry.ownerId);
|
||||
const operation: SyncOperation = {
|
||||
id: createId(),
|
||||
userId: auth.user?.id,
|
||||
|
|
@ -207,6 +210,7 @@ export const useEntryStore = defineStore("entries", {
|
|||
},
|
||||
async updateEntry(entry: LedgerEntry) {
|
||||
const auth = useAuthStore();
|
||||
const db = await getUserDb(auth.user?.id ?? entry.ownerId);
|
||||
const existing = await db.entries.get(entry.id);
|
||||
if (!existing || existing.deletedAt) throw new Error("Entry not found");
|
||||
|
||||
|
|
@ -256,6 +260,9 @@ export const useEntryStore = defineStore("entries", {
|
|||
return updated;
|
||||
},
|
||||
async resolveLocalConversion(entryId: string) {
|
||||
const auth = useAuthStore();
|
||||
if (!auth.user) return;
|
||||
const db = await getUserDb(auth.user.id);
|
||||
const entry = await db.entries.get(entryId);
|
||||
if (!entry || entry.deletedAt || entry.currency === "CNY") return;
|
||||
const cacheId = `${entry.currency}:${entry.exchangeRateDate}`;
|
||||
|
|
@ -322,6 +329,8 @@ export const useEntryStore = defineStore("entries", {
|
|||
},
|
||||
async deleteEntry(entryId: string) {
|
||||
const auth = useAuthStore();
|
||||
if (!auth.user) return;
|
||||
const db = await getUserDb(auth.user.id);
|
||||
const existing = await db.entries.get(entryId);
|
||||
if (!existing || existing.deletedAt) return;
|
||||
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
import { defineStore } from "pinia";
|
||||
import { apiRequest } from "../data/api";
|
||||
import { db } from "../data/db";
|
||||
import { getUserDb } from "../data/db";
|
||||
import { currentLedgerPreferenceId, ledgerAmountDisplayPreferenceId, type LedgerRecord, type UserPreference } from "../data/ledgers";
|
||||
import type { LedgerAmountDisplayMode } from "@cents/domain";
|
||||
import { useAuthStore } from "./auth";
|
||||
|
|
@ -39,6 +39,7 @@ export const useLedgerStore = defineStore("ledgers", {
|
|||
ledgerLoadPromise = (async () => {
|
||||
const auth = useAuthStore();
|
||||
if (!auth.user) return;
|
||||
const db = await getUserDb(auth.user.id);
|
||||
const result = await apiRequest<{ ledgers: LedgerRecord[] }>("/api/ledgers");
|
||||
const displayPreferences = await db.userPreferences
|
||||
.where("userId")
|
||||
|
|
@ -91,6 +92,7 @@ export const useLedgerStore = defineStore("ledgers", {
|
|||
async persistCurrentLedger() {
|
||||
const auth = useAuthStore();
|
||||
if (!this.currentLedgerId || !auth.user) return;
|
||||
const db = await getUserDb(auth.user.id);
|
||||
const preference: UserPreference = {
|
||||
id: currentLedgerPreferenceId(auth.user.id),
|
||||
userId: auth.user.id,
|
||||
|
|
@ -125,6 +127,7 @@ export const useLedgerStore = defineStore("ledgers", {
|
|||
if (amountDisplay) {
|
||||
const auth = useAuthStore();
|
||||
if (auth.user) {
|
||||
const db = await getUserDb(auth.user.id);
|
||||
const preference: UserPreference = {
|
||||
id: ledgerAmountDisplayPreferenceId(auth.user.id, input.id),
|
||||
userId: auth.user.id,
|
||||
|
|
|
|||
Loading…
Reference in New Issue