Skip to content

Commit 6817e2e

Browse files
committed
fix: improve type-safety for KVStore values
1 parent e12e7c1 commit 6817e2e

File tree

7 files changed

+33
-25
lines changed

7 files changed

+33
-25
lines changed

admin/app/controllers/chats_controller.ts

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@ import { inject } from '@adonisjs/core'
22
import type { HttpContext } from '@adonisjs/core/http'
33
import { ChatService } from '#services/chat_service'
44
import { createSessionSchema, updateSessionSchema, addMessageSchema } from '#validators/chat'
5-
import { parseBoolean } from '../utils/misc.js'
65
import KVStore from '#models/kv_store'
76
import { SystemService } from '#services/system_service'
87
import { SERVICE_NAMES } from '../../constants/service_names.js'
@@ -20,7 +19,7 @@ export default class ChatsController {
2019
const chatSuggestionsEnabled = await KVStore.getValue('chat.suggestionsEnabled')
2120
return inertia.render('chat', {
2221
settings: {
23-
chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled),
22+
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false,
2423
},
2524
})
2625
}

admin/app/controllers/settings_controller.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ import { SystemService } from '#services/system_service';
66
import { updateSettingSchema } from '#validators/settings';
77
import { inject } from '@adonisjs/core';
88
import type { HttpContext } from '@adonisjs/core/http'
9-
import { parseBoolean } from '../utils/misc.js';
9+
import type { KVStoreKey } from '../../types/kv_store.js';
1010

1111
@inject()
1212
export default class SettingsController {
@@ -59,7 +59,7 @@ export default class SettingsController {
5959
availableModels: availableModels?.models || [],
6060
installedModels: installedModels || [],
6161
settings: {
62-
chatSuggestionsEnabled: parseBoolean(chatSuggestionsEnabled)
62+
chatSuggestionsEnabled: chatSuggestionsEnabled ?? false
6363
}
6464
}
6565
});
@@ -98,7 +98,7 @@ export default class SettingsController {
9898

9999
async getSetting({ request, response }: HttpContext) {
100100
const key = request.qs().key;
101-
const value = await KVStore.getValue(key);
101+
const value = await KVStore.getValue(key as KVStoreKey);
102102
return response.status(200).send({ key, value });
103103
}
104104

admin/app/jobs/check_update_job.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export class CheckUpdateJob {
2828
`[CheckUpdateJob] Update available: ${result.currentVersion}${result.latestVersion}`
2929
)
3030
} else {
31-
await KVStore.setValue('system.updateAvailable', "false")
31+
await KVStore.setValue('system.updateAvailable', false)
3232
logger.info(
3333
`[CheckUpdateJob] System is up to date (${result.currentVersion})`
3434
)

admin/app/models/kv_store.ts

Lines changed: 13 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import { DateTime } from 'luxon'
22
import { BaseModel, column, SnakeCaseNamingStrategy } from '@adonisjs/lucid/orm'
3-
import type { KVStoreKey, KVStoreValue } from '../../types/kv_store.js'
3+
import { KV_STORE_SCHEMA, type KVStoreKey, type KVStoreValue } from '../../types/kv_store.js'
4+
import { parseBoolean } from '../utils/misc.js'
45

56
/**
67
* Generic key-value store model for storing various settings
@@ -17,7 +18,7 @@ export default class KVStore extends BaseModel {
1718
declare key: KVStoreKey
1819

1920
@column()
20-
declare value: KVStoreValue
21+
declare value: string | null
2122

2223
@column.dateTime({ autoCreate: true })
2324
declare created_at: DateTime
@@ -26,26 +27,25 @@ export default class KVStore extends BaseModel {
2627
declare updated_at: DateTime
2728

2829
/**
29-
* Get a setting value by key
30+
* Get a setting value by key, automatically deserializing to the correct type.
3031
*/
31-
static async getValue(key: KVStoreKey): Promise<KVStoreValue> {
32+
static async getValue<K extends KVStoreKey>(key: K): Promise<KVStoreValue<K> | null> {
3233
const setting = await this.findBy('key', key)
3334
if (!setting || setting.value === undefined || setting.value === null) {
3435
return null
3536
}
36-
if (typeof setting.value === 'string') {
37-
return setting.value
38-
}
39-
return String(setting.value)
37+
const raw = String(setting.value)
38+
return (KV_STORE_SCHEMA[key] === 'boolean' ? parseBoolean(raw) : raw) as KVStoreValue<K>
4039
}
4140

4241
/**
43-
* Set a setting value by key (creates if not exists)
42+
* Set a setting value by key (creates if not exists), automatically serializing to string.
4443
*/
45-
static async setValue(key: KVStoreKey, value: KVStoreValue): Promise<KVStore> {
46-
const setting = await this.firstOrCreate({ key }, { key, value })
47-
if (setting.value !== value) {
48-
setting.value = value
44+
static async setValue<K extends KVStoreKey>(key: K, value: KVStoreValue<K>): Promise<KVStore> {
45+
const serialized = String(value)
46+
const setting = await this.firstOrCreate({ key }, { key, value: serialized })
47+
if (setting.value !== serialized) {
48+
setting.value = serialized
4949
await setting.save()
5050
}
5151
return setting

admin/app/services/docker_service.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -546,7 +546,7 @@ export class DockerService {
546546
// If Ollama was just installed, trigger Nomad docs discovery and embedding
547547
if (service.service_name === SERVICE_NAMES.OLLAMA) {
548548
logger.info('[DockerService] Ollama installation complete. Default behavior is to not enable chat suggestions.')
549-
await KVStore.setValue('chat.suggestionsEnabled', "false")
549+
await KVStore.setValue('chat.suggestionsEnabled', false)
550550

551551
logger.info('[DockerService] Ollama installation complete. Triggering Nomad docs discovery...')
552552

admin/app/services/rag_service.ts

Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -14,7 +14,6 @@ import { removeStopwords } from 'stopword'
1414
import { randomUUID } from 'node:crypto'
1515
import { join } from 'node:path'
1616
import KVStore from '#models/kv_store'
17-
import { parseBoolean } from '../utils/misc.js'
1817
import { ZIMExtractionService } from './zim_extraction_service.js'
1918
import { ZIM_BATCH_SIZE } from '../../constants/zim_extraction.js'
2019

@@ -885,7 +884,7 @@ export class RagService {
885884
const DOCS_DIR = join(process.cwd(), 'docs')
886885

887886
const alreadyEmbeddedRaw = await KVStore.getValue('rag.docsEmbedded')
888-
if (parseBoolean(alreadyEmbeddedRaw) && !force) {
887+
if (alreadyEmbeddedRaw && !force) {
889888
logger.info('[RAG] Nomad docs have already been discovered and queued. Skipping.')
890889
return { success: true, message: 'Nomad docs have already been discovered and queued. Skipping.' }
891890
}
@@ -927,7 +926,7 @@ export class RagService {
927926
}
928927

929928
// Update KV store to mark docs as discovered so we don't redo this unnecessarily
930-
await KVStore.setValue('rag.docsEmbedded', 'true')
929+
await KVStore.setValue('rag.docsEmbedded', true)
931930

932931
return { success: true, message: `Nomad docs discovery completed. Dispatched ${filesToEmbed.length} embedding jobs.` }
933932
} catch (error) {

admin/types/kv_store.ts

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
11

2-
export type KVStoreKey = 'chat.suggestionsEnabled' | 'rag.docsEmbedded' | 'system.updateAvailable' | 'system.latestVersion' | 'ui.hasVisitedEasySetup'
3-
export type KVStoreValue = string | null
2+
export const KV_STORE_SCHEMA = {
3+
'chat.suggestionsEnabled': 'boolean',
4+
'rag.docsEmbedded': 'boolean',
5+
'system.updateAvailable': 'boolean',
6+
'system.latestVersion': 'string',
7+
'ui.hasVisitedEasySetup': 'boolean',
8+
} as const
9+
10+
type KVTagToType<T extends string> = T extends 'boolean' ? boolean : string
11+
12+
export type KVStoreKey = keyof typeof KV_STORE_SCHEMA
13+
export type KVStoreValue<K extends KVStoreKey> = KVTagToType<(typeof KV_STORE_SCHEMA)[K]>

0 commit comments

Comments
 (0)