feat(07-01): add runtime variable CRUD and entity value operations

- Add getRuntimeVariables, getRuntimeVariablesByEntityType for definition queries
- Add createRuntimeVariable, updateRuntimeVariable, deleteRuntimeVariable for definition CRUD
- Add getStoriesUsingPack to find all stories sharing a pack
- Add countEntitiesWithRuntimeVar for deletion warning counts
- Add clearRuntimeVarFromEntities to strip values on definition delete
- Add renameRuntimeVarInEntities to update variableName copy on rename
- All entity operations accept packId and work across all stories sharing the pack
- Add mapRuntimeVariable private helper for row-to-type conversion
This commit is contained in:
munimunigamer 2026-02-17 23:00:17 -06:00
parent 50ddbafbeb
commit 92c9b6501f

View file

@ -25,7 +25,13 @@ import type {
VisualDescriptors,
WorldStateSnapshot,
} from '$lib/types'
import type { PresetPack, PackTemplate, CustomVariable } from '$lib/services/packs/types'
import type {
PresetPack,
PackTemplate,
CustomVariable,
RuntimeVariable,
RuntimeEntityType,
} from '$lib/services/packs/types'
import { hashContent } from '$lib/services/packs/hash'
/**
@ -3321,6 +3327,200 @@ class DatabaseService {
await db.execute('DELETE FROM pack_variables WHERE id = ?', [id])
}
// ===== Runtime Variable Definition Operations =====
async getRuntimeVariables(packId: string): Promise<RuntimeVariable[]> {
const db = await this.getDb()
const results = await db.select<any[]>(
'SELECT * FROM pack_runtime_variables WHERE pack_id = ? ORDER BY entity_type ASC, sort_order ASC, variable_name ASC',
[packId],
)
return results.map(this.mapRuntimeVariable)
}
async getRuntimeVariablesByEntityType(
packId: string,
entityType: RuntimeEntityType,
): Promise<RuntimeVariable[]> {
const db = await this.getDb()
const results = await db.select<any[]>(
'SELECT * FROM pack_runtime_variables WHERE pack_id = ? AND entity_type = ? ORDER BY sort_order ASC, variable_name ASC',
[packId, entityType],
)
return results.map(this.mapRuntimeVariable)
}
async createRuntimeVariable(
packId: string,
variable: Omit<RuntimeVariable, 'id' | 'packId' | 'createdAt'>,
): Promise<RuntimeVariable> {
const db = await this.getDb()
const id = crypto.randomUUID()
const now = Date.now()
await db.execute(
`INSERT INTO pack_runtime_variables (id, pack_id, entity_type, variable_name, display_name, description, variable_type, default_value, min_value, max_value, enum_options, color, icon, sort_order, created_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
[
id,
packId,
variable.entityType,
variable.variableName,
variable.displayName,
variable.description ?? null,
variable.variableType,
variable.defaultValue ?? null,
variable.minValue ?? null,
variable.maxValue ?? null,
variable.enumOptions ? JSON.stringify(variable.enumOptions) : null,
variable.color,
variable.icon ?? null,
variable.sortOrder ?? 0,
now,
],
)
return { id, packId, ...variable, createdAt: now }
}
async updateRuntimeVariable(
id: string,
updates: Partial<Omit<RuntimeVariable, 'id' | 'packId' | 'createdAt'>>,
): Promise<void> {
const db = await this.getDb()
const setClauses: string[] = []
const values: any[] = []
if (updates.entityType !== undefined) {
setClauses.push('entity_type = ?')
values.push(updates.entityType)
}
if (updates.variableName !== undefined) {
setClauses.push('variable_name = ?')
values.push(updates.variableName)
}
if (updates.displayName !== undefined) {
setClauses.push('display_name = ?')
values.push(updates.displayName)
}
if (updates.description !== undefined) {
setClauses.push('description = ?')
values.push(updates.description || null)
}
if (updates.variableType !== undefined) {
setClauses.push('variable_type = ?')
values.push(updates.variableType)
}
if (updates.defaultValue !== undefined) {
setClauses.push('default_value = ?')
values.push(updates.defaultValue || null)
}
if (updates.minValue !== undefined) {
setClauses.push('min_value = ?')
values.push(updates.minValue)
}
if (updates.maxValue !== undefined) {
setClauses.push('max_value = ?')
values.push(updates.maxValue)
}
if (updates.enumOptions !== undefined) {
setClauses.push('enum_options = ?')
values.push(updates.enumOptions ? JSON.stringify(updates.enumOptions) : null)
}
if (updates.color !== undefined) {
setClauses.push('color = ?')
values.push(updates.color)
}
if (updates.icon !== undefined) {
setClauses.push('icon = ?')
values.push(updates.icon || null)
}
if (updates.sortOrder !== undefined) {
setClauses.push('sort_order = ?')
values.push(updates.sortOrder)
}
if (setClauses.length === 0) return
values.push(id)
await db.execute(
`UPDATE pack_runtime_variables SET ${setClauses.join(', ')} WHERE id = ?`,
values,
)
}
async deleteRuntimeVariable(id: string): Promise<void> {
const db = await this.getDb()
await db.execute('DELETE FROM pack_runtime_variables WHERE id = ?', [id])
}
// ===== Runtime Variable Entity Value Operations =====
async getStoriesUsingPack(packId: string): Promise<string[]> {
const db = await this.getDb()
const results = await db.select<any[]>('SELECT id FROM stories WHERE pack_id = ?', [packId])
return results.map((r) => r.id)
}
async countEntitiesWithRuntimeVar(packId: string, defId: string): Promise<number> {
const storyIds = await this.getStoriesUsingPack(packId)
if (storyIds.length === 0) return 0
const db = await this.getDb()
const placeholders = storyIds.map(() => '?').join(', ')
const jsonPath = `$.runtimeVars.${defId}`
const tables = ['characters', 'locations', 'items', 'story_beats']
let total = 0
for (const table of tables) {
const results = await db.select<any[]>(
`SELECT COUNT(*) as cnt FROM ${table} WHERE story_id IN (${placeholders}) AND json_extract(metadata, ?) IS NOT NULL`,
[...storyIds, jsonPath],
)
total += results[0]?.cnt ?? 0
}
return total
}
async clearRuntimeVarFromEntities(packId: string, defId: string): Promise<void> {
const storyIds = await this.getStoriesUsingPack(packId)
if (storyIds.length === 0) return
const db = await this.getDb()
const placeholders = storyIds.map(() => '?').join(', ')
const jsonPath = `$.runtimeVars.${defId}`
const tables = ['characters', 'locations', 'items', 'story_beats']
for (const table of tables) {
await db.execute(
`UPDATE ${table} SET metadata = json_remove(metadata, ?) WHERE story_id IN (${placeholders}) AND json_extract(metadata, ?) IS NOT NULL`,
[jsonPath, ...storyIds, jsonPath],
)
}
}
async renameRuntimeVarInEntities(
packId: string,
defId: string,
newVariableName: string,
): Promise<void> {
const storyIds = await this.getStoriesUsingPack(packId)
if (storyIds.length === 0) return
const db = await this.getDb()
const placeholders = storyIds.map(() => '?').join(', ')
const jsonPath = `$.runtimeVars.${defId}`
const varNamePath = `$.runtimeVars.${defId}.variableName`
const tables = ['characters', 'locations', 'items', 'story_beats']
for (const table of tables) {
await db.execute(
`UPDATE ${table} SET metadata = json_set(metadata, ?, ?) WHERE story_id IN (${placeholders}) AND json_extract(metadata, ?) IS NOT NULL`,
[varNamePath, newVariableName, ...storyIds, jsonPath],
)
}
}
// ===== Story-Pack Assignment =====
async getStoryPackId(storyId: string): Promise<string | null> {
@ -3419,6 +3619,34 @@ class DatabaseService {
}
}
private mapRuntimeVariable(row: any): RuntimeVariable {
return {
id: row.id,
packId: row.pack_id,
entityType: row.entity_type,
variableName: row.variable_name,
displayName: row.display_name,
description: row.description ?? undefined,
variableType: row.variable_type,
defaultValue: row.default_value ?? undefined,
minValue: row.min_value != null ? Number(row.min_value) : undefined,
maxValue: row.max_value != null ? Number(row.max_value) : undefined,
enumOptions: row.enum_options
? (() => {
try {
return JSON.parse(row.enum_options)
} catch {
return undefined
}
})()
: undefined,
color: row.color,
icon: row.icon ?? undefined,
sortOrder: row.sort_order ?? 0,
createdAt: row.created_at,
}
}
private mapPackVariable(row: any): CustomVariable {
return {
id: row.id,