Adding fixes that lets us pull a myster in the db into a json file
This commit is contained in:
@@ -397,6 +397,14 @@ app.delete('/api/admin/npcs/:id/poses/:poseKey', requireAdmin, async (req, res,
|
||||
app.get('/api/admin/level-templates', requireAdmin, async (_req, res, next) => {
|
||||
try { res.json(await storyGraph.listLevelTemplates()) } catch (error) { next(error) }
|
||||
})
|
||||
// Read a frozen template version's board (exhibits, goals, rules) for the mystery:pull
|
||||
// exporter — read-only, so no editing gate.
|
||||
app.get('/api/admin/level-templates/:versionId/export', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const board = await levels.getTemplateBoard(String(req.params.versionId))
|
||||
board ? res.json(board) : res.status(404).json({ error: 'Template version not found' })
|
||||
} catch (error) { next(error) }
|
||||
})
|
||||
app.get('/api/admin/mysteries/:id/graph', requireAdmin, async (req, res, next) => {
|
||||
try {
|
||||
const graph = await storyGraph.getGraph(String(req.params.id))
|
||||
|
||||
@@ -15,6 +15,9 @@ export type UploadedDocument = { buffer: Buffer; originalname: string; mimetype:
|
||||
export type AssetRecord = { original_name: string; mime_type: string; byte_size: string; content: Buffer | null; storage_provider: 'postgres' | 's3'; object_key: string | null }
|
||||
export type AssetResponse = { originalName: string; mimeType: string; byteSize: number; stream: NodeJS.ReadableStream }
|
||||
export type TemplateSummary = { id: string; slug: string; name: string; currentVersion: number; versionCount: number; updatedAt: string }
|
||||
// A frozen template version's board, read back for round-trip export. `state.goals`
|
||||
// carries the goals (in author mode); match/semantic rules are read alongside.
|
||||
export type TemplateBoardExport = { slug: string; name: string; version: number; state: CaseState; matchRules: EvidenceMatchRuleDefinition[]; semanticRules: EvidenceSemanticRuleDefinition[] }
|
||||
export type EvidenceMatchRuleInput = {
|
||||
name: string
|
||||
sourceLabel?: string
|
||||
@@ -51,6 +54,7 @@ export interface LevelRepository {
|
||||
listTemplates(): Promise<TemplateSummary[]>
|
||||
instantiateTemplate(templateSlug: string, input: { id: string; title?: string; version?: number }): Promise<CaseState | null>
|
||||
saveLevelAsTemplate(levelId: string, input: { slug: string; name: string }): Promise<TemplateSummary | null>
|
||||
getTemplateBoard(versionId: string): Promise<TemplateBoardExport | null>
|
||||
getLevel(levelId: string, authorMode?: boolean): Promise<CaseState | null>
|
||||
saveLevel(state: CaseState, authorMode: boolean): Promise<void>
|
||||
resetLevel(levelId: string): Promise<CaseState | null>
|
||||
@@ -347,6 +351,14 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
async function assembleLevel(slug: string, authorMode = false): Promise<CaseState | null> {
|
||||
const level = await findLevel(pool, slug)
|
||||
if (!level) return null
|
||||
return assembleBoard(level, authorMode)
|
||||
}
|
||||
|
||||
// Assemble a board's CaseState from a level row (id + board_id). Split out from
|
||||
// assembleLevel so a frozen template version's board can be read the same way (its
|
||||
// board_id backs the same tables); player-state reads (flags/seen) return empty for
|
||||
// a template board that no player has instantiated.
|
||||
async function assembleBoard(level: LevelRow, authorMode = false): Promise<CaseState> {
|
||||
const [exhibitsResult, blocksResult, regionsResult, membershipsResult, connectionsResult, metadataResult, eventEvidenceResult,
|
||||
aliasesResult, partyEvidenceResult, briefResult, conceptsResult, viewsResult, requirementsResult, flagsResult, seenResult] = await Promise.all([
|
||||
pool.query<ExhibitRow>(`SELECT e.id,e.exhibit_type_id,e.xpos,e.ypos,e.width,e.height,e.rotation,e.z_index,e.hidden,
|
||||
@@ -739,6 +751,23 @@ export function createLevelRepository(pool: Pool, editingEnabled: boolean, objec
|
||||
} catch (error) { await client.query('ROLLBACK'); throw error } finally { client.release() }
|
||||
return templateSummary(input.slug)
|
||||
},
|
||||
async getTemplateBoard(versionId) {
|
||||
const version = (await pool.query<{ board_id: string; title: string; subtitle: string; version: number; slug: string; name: string }>(
|
||||
`SELECT v.board_id,v.title,v.subtitle,v.version,t.slug,t.name FROM osint.level_template_versions v
|
||||
JOIN osint.level_templates t ON t.id=v.template_id WHERE v.id=$1`, [versionId])).rows[0]
|
||||
if (!version) return null
|
||||
// A synthetic level row over the version's board: id has no player rows (flags/seen
|
||||
// resolve empty); viewport/revision/status are placeholders the exporter discards.
|
||||
const level: LevelRow = {
|
||||
id: versionId, slug: version.slug, board_id: version.board_id, title: version.title, subtitle: version.subtitle,
|
||||
status: 'template', viewport_x: 0, viewport_y: 28, viewport_zoom: 0.7, updated_at: new Date(), revision: '0',
|
||||
source_template_version_id: null,
|
||||
}
|
||||
const [state, matchRules, semanticRules] = await Promise.all([
|
||||
assembleBoard(level, true), evidenceMatchRules(pool, version.board_id, true), evidenceSemanticRules(pool, version.board_id, true),
|
||||
])
|
||||
return { slug: version.slug, name: version.name, version: version.version, state, matchRules, semanticRules }
|
||||
},
|
||||
getLevel(levelId, authorMode = false) { return assembleLevel(levelId, authorMode) },
|
||||
async saveLevel(state, authorMode) {
|
||||
let persistedState = state
|
||||
|
||||
@@ -25,7 +25,7 @@ export type LevelTemplateOption = { versionId: string; slug: string; name: strin
|
||||
export type Utterer = 'npc' | 'player'
|
||||
export type UtteranceDto = {
|
||||
id: string; nodeId: string; utterer: Utterer; npcId: string | null; poseKey: string | null; text: string
|
||||
parentUtteranceId: string | null; terminalId: string | null
|
||||
parentUtteranceId: string | null; terminalId: string | null; awardsFlag: string | null; requiresFlag: string | null
|
||||
xpos: number; ypos: number; sortOrder: number
|
||||
}
|
||||
|
||||
@@ -190,7 +190,7 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository {
|
||||
|
||||
async listUtterances(nodeId) {
|
||||
const result = await pool.query<UtteranceRow>(
|
||||
`SELECT id,node_id,utterer,npc_id,pose_key,text,parent_utterance_id,terminal_id,xpos,ypos,sort_order
|
||||
`SELECT id,node_id,utterer,npc_id,pose_key,text,parent_utterance_id,terminal_id,awards_flag,requires_flag,xpos,ypos,sort_order
|
||||
FROM osint.utterances WHERE node_id=$1 ORDER BY sort_order,id`, [nodeId])
|
||||
return result.rows.map(mapUtterance)
|
||||
},
|
||||
@@ -203,7 +203,7 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository {
|
||||
await pool.query('INSERT INTO osint.utterances (id,node_id,utterer,text,xpos,ypos,sort_order) VALUES ($1,$2,$3,$4,$5,$6,$7)',
|
||||
[id, nodeId, input.utterer, input.text || '', input.xpos, input.ypos, order.rows[0].next])
|
||||
const created = await pool.query<UtteranceRow>(
|
||||
`SELECT id,node_id,utterer,npc_id,pose_key,text,parent_utterance_id,terminal_id,xpos,ypos,sort_order FROM osint.utterances WHERE id=$1`, [id])
|
||||
`SELECT id,node_id,utterer,npc_id,pose_key,text,parent_utterance_id,terminal_id,awards_flag,requires_flag,xpos,ypos,sort_order FROM osint.utterances WHERE id=$1`, [id])
|
||||
return mapUtterance(created.rows[0])
|
||||
},
|
||||
|
||||
@@ -333,13 +333,14 @@ export function createStoryGraphRepository(pool: Pool): StoryGraphRepository {
|
||||
|
||||
type UtteranceRow = {
|
||||
id: string; node_id: string; utterer: Utterer; npc_id: string | null; pose_key: string | null; text: string
|
||||
parent_utterance_id: string | null; terminal_id: string | null
|
||||
parent_utterance_id: string | null; terminal_id: string | null; awards_flag: string | null; requires_flag: string | null
|
||||
xpos: number; ypos: number; sort_order: number
|
||||
}
|
||||
function mapUtterance(row: UtteranceRow): UtteranceDto {
|
||||
return {
|
||||
id: row.id, nodeId: row.node_id, utterer: row.utterer, npcId: row.npc_id, poseKey: row.pose_key, text: row.text,
|
||||
parentUtteranceId: row.parent_utterance_id, terminalId: row.terminal_id,
|
||||
awardsFlag: row.awards_flag, requiresFlag: row.requires_flag,
|
||||
xpos: row.xpos, ypos: row.ypos, sortOrder: row.sort_order,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user