switch from next to vue
This commit is contained in:
+158
@@ -0,0 +1,158 @@
|
||||
import initSqlJs, { type Database } from 'sql.js'
|
||||
import wasmUrl from 'sql.js/dist/sql-wasm.wasm?url'
|
||||
import type { Entry } from './types'
|
||||
|
||||
const DB_NAME = 'sanasto-sqlite'
|
||||
const STORE_NAME = 'sqlite'
|
||||
const DB_KEY = 'db'
|
||||
|
||||
let sqlInit: ReturnType<typeof initSqlJs> | null = null
|
||||
|
||||
function initSql(): ReturnType<typeof initSqlJs> {
|
||||
if (!sqlInit) {
|
||||
sqlInit = initSqlJs({ locateFile: () => wasmUrl })
|
||||
}
|
||||
return sqlInit
|
||||
}
|
||||
|
||||
export async function initDb(): Promise<Database> {
|
||||
const SQL = await initSql()
|
||||
const stored = await loadDbFromIdb()
|
||||
const db = stored ? new SQL.Database(new Uint8Array(stored)) : new SQL.Database()
|
||||
ensureSchema(db)
|
||||
return db
|
||||
}
|
||||
|
||||
function ensureSchema(db: Database) {
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS entries (
|
||||
id INTEGER PRIMARY KEY,
|
||||
category TEXT,
|
||||
fi TEXT,
|
||||
en TEXT,
|
||||
sv TEXT,
|
||||
no TEXT,
|
||||
ru TEXT,
|
||||
de TEXT,
|
||||
updated_at TEXT
|
||||
);
|
||||
`)
|
||||
db.exec(`
|
||||
CREATE TABLE IF NOT EXISTS meta (
|
||||
key TEXT PRIMARY KEY,
|
||||
value TEXT
|
||||
);
|
||||
`)
|
||||
}
|
||||
|
||||
export function listEntries(db: Database): Entry[] {
|
||||
const result = db.exec(
|
||||
`SELECT id, category, fi, en, sv, no, ru, de, updated_at
|
||||
FROM entries
|
||||
ORDER BY COALESCE(fi, en, no, sv, de, ru, '') ASC`
|
||||
)
|
||||
const [first] = result
|
||||
if (!first) {
|
||||
return []
|
||||
}
|
||||
const { columns, values } = first
|
||||
return values.map((row: unknown[]) => {
|
||||
const entry: Record<string, unknown> = {}
|
||||
columns.forEach((column: string, index: number) => {
|
||||
entry[column] = row[index] ?? null
|
||||
})
|
||||
return entry as Entry
|
||||
})
|
||||
}
|
||||
|
||||
export function upsertEntries(db: Database, entries: Entry[]) {
|
||||
const statement = db.prepare(
|
||||
`INSERT INTO entries (id, category, fi, en, sv, no, ru, de, updated_at)
|
||||
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
ON CONFLICT(id) DO UPDATE SET
|
||||
category=excluded.category,
|
||||
fi=excluded.fi,
|
||||
en=excluded.en,
|
||||
sv=excluded.sv,
|
||||
no=excluded.no,
|
||||
ru=excluded.ru,
|
||||
de=excluded.de,
|
||||
updated_at=excluded.updated_at`
|
||||
)
|
||||
entries.forEach((entry) => {
|
||||
statement.run([
|
||||
entry.id,
|
||||
entry.category ?? null,
|
||||
entry.fi ?? null,
|
||||
entry.en ?? null,
|
||||
entry.sv ?? null,
|
||||
entry.no ?? null,
|
||||
entry.ru ?? null,
|
||||
entry.de ?? null,
|
||||
entry.updated_at ?? null,
|
||||
])
|
||||
})
|
||||
statement.free()
|
||||
}
|
||||
|
||||
export function getMeta(db: Database, key: string): string | null {
|
||||
const result = db.exec(`SELECT value FROM meta WHERE key = ?`, [key])
|
||||
const first = result[0]
|
||||
if (!first || !first.values.length || !first.values[0]?.length) {
|
||||
return null
|
||||
}
|
||||
const value = first.values[0][0]
|
||||
return typeof value === 'string' ? value : null
|
||||
}
|
||||
|
||||
export function setMeta(db: Database, key: string, value: string) {
|
||||
db.exec(`INSERT INTO meta (key, value) VALUES (?, ?)
|
||||
ON CONFLICT(key) DO UPDATE SET value=excluded.value`, [key, value])
|
||||
}
|
||||
|
||||
export async function persistDb(db: Database) {
|
||||
const data = db.export()
|
||||
await saveDbToIdb(data)
|
||||
}
|
||||
|
||||
function openIdb(): Promise<IDBDatabase> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const request = indexedDB.open(DB_NAME, 1)
|
||||
request.onupgradeneeded = () => {
|
||||
const db = request.result
|
||||
if (!db.objectStoreNames.contains(STORE_NAME)) {
|
||||
db.createObjectStore(STORE_NAME)
|
||||
}
|
||||
}
|
||||
request.onsuccess = () => resolve(request.result)
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
}
|
||||
|
||||
function loadDbFromIdb(): Promise<ArrayBuffer | null> {
|
||||
return openIdb().then(
|
||||
(db) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(STORE_NAME, 'readonly')
|
||||
const store = transaction.objectStore(STORE_NAME)
|
||||
const request = store.get(DB_KEY)
|
||||
request.onsuccess = () => {
|
||||
resolve((request.result as ArrayBuffer | undefined) ?? null)
|
||||
}
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
function saveDbToIdb(data: Uint8Array): Promise<void> {
|
||||
return openIdb().then(
|
||||
(db) =>
|
||||
new Promise((resolve, reject) => {
|
||||
const transaction = db.transaction(STORE_NAME, 'readwrite')
|
||||
const store = transaction.objectStore(STORE_NAME)
|
||||
const request = store.put(data, DB_KEY)
|
||||
request.onsuccess = () => resolve()
|
||||
request.onerror = () => reject(request.error)
|
||||
})
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
import type { Database } from 'sql.js'
|
||||
import type { Entry } from './types'
|
||||
import { getMeta, persistDb, setMeta, upsertEntries } from './db'
|
||||
|
||||
const LAST_SYNC_KEY = 'last_sync_at'
|
||||
|
||||
export async function syncEntries(db: Database) {
|
||||
const lastSyncAt = getMeta(db, LAST_SYNC_KEY)
|
||||
const url = new URL('https://sanasto.rin.no/api/entries')
|
||||
if (lastSyncAt) {
|
||||
url.searchParams.set('since', lastSyncAt)
|
||||
}
|
||||
|
||||
const response = await fetch(url.toString(), {
|
||||
headers: {
|
||||
'X-Sanasto-App': 'app.sanasto',
|
||||
},
|
||||
})
|
||||
if (!response.ok) {
|
||||
throw new Error(`Sync failed with ${response.status}`)
|
||||
}
|
||||
|
||||
const entries = (await response.json()) as Entry[]
|
||||
if (entries.length === 0) {
|
||||
return { updated: 0, lastSyncAt }
|
||||
}
|
||||
|
||||
upsertEntries(db, entries)
|
||||
|
||||
const newest = entries.reduce<Date | null>((current, entry) => {
|
||||
if (!entry.updated_at) {
|
||||
return current
|
||||
}
|
||||
const date = new Date(entry.updated_at)
|
||||
if (!current || date > current) {
|
||||
return date
|
||||
}
|
||||
return current
|
||||
}, lastSyncAt ? new Date(lastSyncAt) : null)
|
||||
|
||||
if (newest) {
|
||||
setMeta(db, LAST_SYNC_KEY, newest.toISOString())
|
||||
}
|
||||
|
||||
await persistDb(db)
|
||||
|
||||
return { updated: entries.length, lastSyncAt: newest?.toISOString() ?? lastSyncAt }
|
||||
}
|
||||
@@ -0,0 +1,16 @@
|
||||
export type Entry = {
|
||||
id: number
|
||||
category: string | null
|
||||
fi: string | null
|
||||
en: string | null
|
||||
sv: string | null
|
||||
no: string | null
|
||||
ru: string | null
|
||||
de: string | null
|
||||
updated_at: string | null
|
||||
}
|
||||
|
||||
export type Language = {
|
||||
code: keyof Pick<Entry, 'fi' | 'en' | 'sv' | 'no' | 'ru' | 'de'>
|
||||
name: string
|
||||
}
|
||||
Reference in New Issue
Block a user