fix: improve type-safety for KVStore values

This commit is contained in:
Jake Turner 2026-03-04 02:50:26 +00:00 committed by Jake Turner
parent e12e7c1696
commit 6817e2e47e
7 changed files with 33 additions and 25 deletions

View File

@ -2,7 +2,6 @@ import { inject } from '@adonisjs/core'
import type { HttpContext } from '@adonisjs/core/http'
import { ChatService } from '#services/chat_service'
import { createSessionSchema, updateSessionSchema, addMessageSchema } from '#validators/chat'
import { parseBoolean } from '../utils/misc.js'
import KVStore from '#models/kv_store'
import { SystemService } from '#services/system_service'
import { SERVICE_NAMES } from '../../constants/service_names.js'
@ -20,7 +19,7 @@ export default class ChatsController {
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
return inertia.render('chat', {
settings: {
chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled),
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false,
},
})
}

View File

@ -6,7 +6,7 @@ import { SystemService } from '#services/system_service';
import { updateSettingSchema } from '#validators/settings';
import { inject } from '@adonisjs/core';
import type { HttpContext } from '@adonisjs/core/http'
import { parseBoolean } from '../utils/misc.js';
import type { KVStoreKey } from '../../types/kv_store.js';
@inject()
export default class SettingsController {
@ -59,7 +59,7 @@ export default class SettingsController {
availableModels: availableModels?.models || [],
installedModels: installedModels || [],
settings: {
chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled)
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false
}
}
});
@ -98,7 +98,7 @@ export default class SettingsController {
async getSetting({ request, response }: HttpContext) {
const key = request.qs().key;
const value = await KVStore.getValue(key);
const value = await KVStore.getValue(key as KVStoreKey);
return response.status(200).send({ key, value });
}

View File

@ -28,7 +28,7 @@ export class CheckUpdateJob {
`[CheckUpdateJob] Update available: ${result.currentVersion}${result.latestVersion}`
)
} else {
await KVStore.setValue('system.updateAvailable', "false")
await KVStore.setValue('system.updateAvailable', false)
logger.info(
`[CheckUpdateJob] System is up to date (${result.currentVersion})`
)

View File

@ -1,6 +1,7 @@
import { DateTime } from 'luxon'
import { BaseModel, column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm'
import type { KVStoreKey, KVStoreValue } from '../../types/kv_store.js'
import { KV_STORE_SCHEMA, type KVStoreKey, type KVStoreValue } from '../../types/kv_store.js'
import { parseBoolean } from '../utils/misc.js'
/**
* Generic key-value store model for storing various settings
@ -17,7 +18,7 @@ export default class KVStore extends BaseModel {
declare key: KVStoreKey
@column()
declare value: KVStoreValue
declare value: string | null
@column.dateTime({ autoCreate: true })
declare created_at: DateTime
@ -26,26 +27,25 @@ export default class KVStore extends BaseModel {
declare updated_at: DateTime
/**
* Get a setting value by key
* Get a setting value by key, automatically deserializing to the correct type.
*/
static async getValue(key: KVStoreKey): Promise<KVStoreValue> {
static async getValue<K extends KVStoreKey>(key: K): Promise<KVStoreValue<K> | null> {
const setting = await this.findBy('key', key)
if (!setting || setting.value === undefined || setting.value === null) {
return null
}
if (typeof setting.value === 'string') {
return setting.value
}
return String(setting.value)
const raw = String(setting.value)
return (KV_STORE_SCHEMA[key] === 'boolean' ? parseBoolean(raw) : raw) as KVStoreValue<K>
}
/**
* Set a setting value by key (creates if not exists)
* Set a setting value by key (creates if not exists), automatically serializing to string.
*/
static async setValue(key: KVStoreKey, value: KVStoreValue): Promise<KVStore> {
const setting = await this.firstOrCreate({ key }, { key, value })
if (setting.value !== value) {
setting.value = value
static async setValue<K extends KVStoreKey>(key: K, value: KVStoreValue<K>): Promise<KVStore> {
const serialized = String(value)
const setting = await this.firstOrCreate({ key }, { key, value: serialized })
if (setting.value !== serialized) {
setting.value = serialized
await setting.save()
}
return setting

View File

@ -546,7 +546,7 @@ export class DockerService {
// If Ollama was just installed, trigger Nomad docs discovery and embedding
if (service.service_name === SERVICE_NAMES.OLLAMA) {
logger.info('[DockerService] Ollama installation complete. Default behavior is to not enable chat suggestions.')
await KVStore.setValue('chat.suggestionsEnabled', "false")
await KVStore.setValue('chat.suggestionsEnabled', false)
logger.info('[DockerService] Ollama installation complete. Triggering Nomad docs discovery...')

View File

@ -14,7 +14,6 @@ import { removeStopwords } from 'stopword'
import { randomUUID } from 'node:crypto'
import { join } from 'node:path'
import KVStore from '#models/kv_store'
import { parseBoolean } from '../utils/misc.js'
import { ZIMExtractionService } from './zim_extraction_service.js'
import { ZIM_BATCH_SIZE } from '../../constants/zim_extraction.js'
@ -885,7 +884,7 @@ export class RagService {
const DOCS_DIR = join(process.cwd(), 'docs')
const alreadyEmbeddedRaw = await KVStore.getValue('rag.docsEmbedded')
if (parseBoolean(alreadyEmbeddedRaw) && !force) {
if (alreadyEmbeddedRaw && !force) {
logger.info('[RAG] Nomad docs have already been discovered and queued. Skipping.')
return { success: true, message: 'Nomad docs have already been discovered and queued. Skipping.' }
}
@ -927,7 +926,7 @@ export class RagService {
}
// Update KV store to mark docs as discovered so we don't redo this unnecessarily
await KVStore.setValue('rag.docsEmbedded', 'true')
await KVStore.setValue('rag.docsEmbedded', true)
return { success: true, message: `Nomad docs discovery completed. Dispatched ${filesToEmbed.length} embedding jobs.` }
} catch (error) {

View File

@ -1,3 +1,13 @@
export type KVStoreKey = 'chat.suggestionsEnabled' | 'rag.docsEmbedded' | 'system.updateAvailable' | 'system.latestVersion' | 'ui.hasVisitedEasySetup'
export type KVStoreValue = string | null
export const KV_STORE_SCHEMA = {
'chat.suggestionsEnabled': 'boolean',
'rag.docsEmbedded': 'boolean',
'system.updateAvailable': 'boolean',
'system.latestVersion': 'string',
'ui.hasVisitedEasySetup': 'boolean',
} as const
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string
export type KVStoreKey = keyof typeof KV_STORE_SCHEMA
export type KVStoreValue<K extends KVStoreKey> = KVTagToType<(typeof KV_STORE_SCHEMA)[K]>