606 lines
20 KiB
JavaScript
606 lines
20 KiB
JavaScript
import { queryOne, queryRequired } from './db.js';
|
|||
|
|
function isPlainObject(value) {
|
||
|
|
return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
|
||
|
|
}
|
||
|
|
export function mergeSceneParams(base, patch) {
|
||
|
|
const next = { ...base };
|
||
|
|
for (const [key, value] of Object.entries(patch)) {
|
||
|
|
if (value === null) {
|
||
|
|
delete next[key];
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const current = next[key];
|
||
|
|
if (isPlainObject(current) && isPlainObject(value)) {
|
||
|
|
next[key] = mergeSceneParams(current, value);
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
next[key] = value;
|
||
|
|
}
|
||
|
|
return next;
|
||
|
|
}
|
||
|
|
function parseJsonObject(value) {
|
||
|
|
if (typeof value === 'string')
|
||
|
|
return JSON.parse(value);
|
||
|
|
return (value ?? {});
|
||
|
|
}
|
||
|
|
function normalizePresetNode(row) {
|
||
|
|
return { ...row, params: parseJsonObject(row.params) };
|
||
|
|
}
|
||
|
|
function normalizeDeck(row) {
|
||
|
|
return { ...row, params: parseJsonObject(row.params) };
|
||
|
|
}
|
||
|
|
function normalizeScene(row) {
|
||
|
|
return {
|
||
|
|
...row,
|
||
|
|
params: parseJsonObject(row.params),
|
||
|
|
transition: parseJsonObject(row.transition)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function normalizeInitialConditionGenerator(row) {
|
||
|
|
return {
|
||
|
|
...row,
|
||
|
|
supported_classes: parseJsonObject(row.supported_classes),
|
||
|
|
params_schema: parseJsonObject(row.params_schema),
|
||
|
|
default_params: parseJsonObject(row.default_params)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function normalizeCaEngine(row) {
|
||
|
|
return {
|
||
|
|
...row,
|
||
|
|
supported_classes: parseJsonObject(row.supported_classes),
|
||
|
|
params_schema: parseJsonObject(row.params_schema),
|
||
|
|
default_params: parseJsonObject(row.default_params)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
function normalizeStates(states) {
|
||
|
|
return Array.isArray(states) ? [...states].sort() : states;
|
||
|
|
}
|
||
|
|
function caClassEquals(left, right) {
|
||
|
|
return (left.dimensions === right.dimensions &&
|
||
|
|
JSON.stringify(normalizeStates(left.states)) === JSON.stringify(normalizeStates(right.states)) &&
|
||
|
|
neighborhoodsCompatible(left, right));
|
||
|
|
}
|
||
|
|
function neighborhoodsCompatible(left, right) {
|
||
|
|
if (!left.neighborhoodId || !right.neighborhoodId)
|
||
|
|
return true;
|
||
|
|
return left.neighborhoodId === right.neighborhoodId;
|
||
|
|
}
|
||
|
|
export async function createPresetTree(db, input) {
|
||
|
|
return queryRequired(db, `
|
||
|
|
INSERT INTO ca_preset_trees (slug, name, description, schema_version)
|
||
|
|
VALUES ($1, $2, $3, $4)
|
||
|
|
RETURNING *
|
||
|
|
`, [input.slug, input.name, input.description ?? null, input.schemaVersion ?? 1]);
|
||
|
|
}
|
||
|
|
export async function listPresetTrees(db) {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT
|
||
|
|
tree.*,
|
||
|
|
COUNT(node.id)::integer AS node_count
|
||
|
|
FROM ca_preset_trees tree
|
||
|
|
LEFT JOIN ca_preset_nodes node
|
||
|
|
ON node.tree_id = tree.id
|
||
|
|
AND node.archived_at IS NULL
|
||
|
|
WHERE tree.archived_at IS NULL
|
||
|
|
GROUP BY tree.id
|
||
|
|
ORDER BY tree.created_at DESC, tree.name
|
||
|
|
`);
|
||
|
|
return result.rows;
|
||
|
|
}
|
||
|
|
export async function deletePresetTree(db, treeId) {
|
||
|
|
const result = await db.query('DELETE FROM ca_preset_trees WHERE id = $1 RETURNING id', [treeId]);
|
||
|
|
return result.rows.length > 0;
|
||
|
|
}
|
||
|
|
export async function createPresetNode(db, input) {
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
INSERT INTO ca_preset_nodes (
|
||
|
|
tree_id, parent_id, slug, name, kind, description, notes, sort_order, schema_version, params
|
||
|
|
)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb)
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
input.treeId,
|
||
|
|
input.parentId ?? null,
|
||
|
|
input.slug,
|
||
|
|
input.name,
|
||
|
|
input.kind ?? 'scene_base',
|
||
|
|
input.description ?? null,
|
||
|
|
input.notes ?? null,
|
||
|
|
input.sortOrder ?? 0,
|
||
|
|
input.schemaVersion ?? 1,
|
||
|
|
JSON.stringify(input.params ?? {})
|
||
|
|
]);
|
||
|
|
return normalizePresetNode(row);
|
||
|
|
}
|
||
|
|
export async function updatePresetNode(db, treeId, nodeId, patch) {
|
||
|
|
const existing = await getPresetNode(db, treeId, nodeId);
|
||
|
|
if (!existing)
|
||
|
|
return null;
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
UPDATE ca_preset_nodes
|
||
|
|
SET
|
||
|
|
parent_id = $3,
|
||
|
|
slug = $4,
|
||
|
|
name = $5,
|
||
|
|
kind = $6,
|
||
|
|
description = $7,
|
||
|
|
notes = $8,
|
||
|
|
sort_order = $9,
|
||
|
|
schema_version = $10,
|
||
|
|
params = $11::jsonb,
|
||
|
|
updated_at = now()
|
||
|
|
WHERE tree_id = $1 AND id = $2
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
treeId,
|
||
|
|
nodeId,
|
||
|
|
Object.hasOwn(patch, 'parentId') ? patch.parentId : existing.parent_id,
|
||
|
|
patch.slug ?? existing.slug,
|
||
|
|
patch.name ?? existing.name,
|
||
|
|
patch.kind ?? existing.kind,
|
||
|
|
Object.hasOwn(patch, 'description') ? patch.description : existing.description,
|
||
|
|
Object.hasOwn(patch, 'notes') ? patch.notes : existing.notes,
|
||
|
|
patch.sortOrder ?? existing.sort_order,
|
||
|
|
patch.schemaVersion ?? existing.schema_version,
|
||
|
|
JSON.stringify(patch.params ?? existing.params)
|
||
|
|
]);
|
||
|
|
return normalizePresetNode(row);
|
||
|
|
}
|
||
|
|
export async function getPresetNode(db, treeId, nodeId) {
|
||
|
|
const row = await queryOne(db, 'SELECT * FROM ca_preset_nodes WHERE tree_id = $1 AND id = $2', [treeId, nodeId]);
|
||
|
|
return row ? normalizePresetNode(row) : null;
|
||
|
|
}
|
||
|
|
export async function listPresetNodes(db, treeId) {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT *
|
||
|
|
FROM ca_preset_nodes
|
||
|
|
WHERE tree_id = $1
|
||
|
|
ORDER BY parent_id NULLS FIRST, sort_order, slug
|
||
|
|
`, [treeId]);
|
||
|
|
return result.rows.map(normalizePresetNode);
|
||
|
|
}
|
||
|
|
export async function listPresetNodeUsage(db, treeId) {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT preset_node_id AS node_id, count(*) AS scene_count
|
||
|
|
FROM ca_scenes
|
||
|
|
WHERE preset_tree_id = $1
|
||
|
|
AND preset_node_id IS NOT NULL
|
||
|
|
AND archived_at IS NULL
|
||
|
|
GROUP BY preset_node_id
|
||
|
|
`, [treeId]);
|
||
|
|
return result.rows.map((row) => ({
|
||
|
|
node_id: row.node_id,
|
||
|
|
scene_count: Number(row.scene_count)
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
export async function deletePresetNode(db, treeId, nodeId) {
|
||
|
|
const result = await db.query('DELETE FROM ca_preset_nodes WHERE tree_id = $1 AND id = $2 RETURNING id', [treeId, nodeId]);
|
||
|
|
return result.rows.length > 0;
|
||
|
|
}
|
||
|
|
export async function resolvePresetNode(db, treeId, nodeId) {
|
||
|
|
const result = await db.query(`
|
||
|
|
WITH RECURSIVE ancestry AS (
|
||
|
|
SELECT *, 0 AS depth
|
||
|
|
FROM ca_preset_nodes
|
||
|
|
WHERE tree_id = $1 AND id = $2
|
||
|
|
|
||
|
|
UNION ALL
|
||
|
|
|
||
|
|
SELECT parent.*, child.depth + 1 AS depth
|
||
|
|
FROM ca_preset_nodes parent
|
||
|
|
JOIN ancestry child
|
||
|
|
ON parent.tree_id = child.tree_id
|
||
|
|
AND parent.id = child.parent_id
|
||
|
|
)
|
||
|
|
SELECT *
|
||
|
|
FROM ancestry
|
||
|
|
ORDER BY depth DESC
|
||
|
|
`, [treeId, nodeId]);
|
||
|
|
if (result.rows.length === 0)
|
||
|
|
return null;
|
||
|
|
const ancestry = result.rows.map((row) => normalizePresetNode(row));
|
||
|
|
const params = ancestry.reduce((resolved, node) => mergeSceneParams(resolved, node.params), {});
|
||
|
|
return {
|
||
|
|
treeId,
|
||
|
|
nodeId,
|
||
|
|
ancestry,
|
||
|
|
params
|
||
|
|
};
|
||
|
|
}
|
||
|
|
export async function createInitialConditionGenerator(db, input) {
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
INSERT INTO ca_initial_condition_generators (
|
||
|
|
slug,
|
||
|
|
name,
|
||
|
|
description,
|
||
|
|
generator_kind,
|
||
|
|
supported_classes,
|
||
|
|
params_schema,
|
||
|
|
default_params
|
||
|
|
)
|
||
|
|
VALUES ($1, $2, $3, $4, $5::jsonb, $6::jsonb, $7::jsonb)
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
input.slug,
|
||
|
|
input.name,
|
||
|
|
input.description ?? null,
|
||
|
|
input.generatorKind,
|
||
|
|
JSON.stringify(input.supportedClasses),
|
||
|
|
JSON.stringify(input.paramsSchema ?? {}),
|
||
|
|
JSON.stringify(input.defaultParams ?? {})
|
||
|
|
]);
|
||
|
|
return normalizeInitialConditionGenerator(row);
|
||
|
|
}
|
||
|
|
export async function listInitialConditionGenerators(db, filter = {}) {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT *
|
||
|
|
FROM ca_initial_condition_generators
|
||
|
|
WHERE archived_at IS NULL
|
||
|
|
ORDER BY name, slug
|
||
|
|
`);
|
||
|
|
const generators = result.rows.map(normalizeInitialConditionGenerator);
|
||
|
|
if (!filter.caClass)
|
||
|
|
return generators;
|
||
|
|
return generators.filter((generator) => generator.supported_classes.some((supportedClass) => caClassEquals(supportedClass, filter.caClass)));
|
||
|
|
}
|
||
|
|
export async function getInitialConditionGenerator(db, generatorId) {
|
||
|
|
const row = await queryOne(db, 'SELECT * FROM ca_initial_condition_generators WHERE id = $1', [generatorId]);
|
||
|
|
return row ? normalizeInitialConditionGenerator(row) : null;
|
||
|
|
}
|
||
|
|
export async function updateInitialConditionGenerator(db, generatorId, patch) {
|
||
|
|
const existing = await getInitialConditionGenerator(db, generatorId);
|
||
|
|
if (!existing)
|
||
|
|
return null;
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
UPDATE ca_initial_condition_generators
|
||
|
|
SET
|
||
|
|
slug = $2,
|
||
|
|
name = $3,
|
||
|
|
description = $4,
|
||
|
|
generator_kind = $5,
|
||
|
|
supported_classes = $6::jsonb,
|
||
|
|
params_schema = $7::jsonb,
|
||
|
|
default_params = $8::jsonb,
|
||
|
|
updated_at = now()
|
||
|
|
WHERE id = $1
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
generatorId,
|
||
|
|
patch.slug ?? existing.slug,
|
||
|
|
patch.name ?? existing.name,
|
||
|
|
Object.hasOwn(patch, 'description') ? patch.description : existing.description,
|
||
|
|
patch.generatorKind ?? existing.generator_kind,
|
||
|
|
JSON.stringify(patch.supportedClasses ?? existing.supported_classes),
|
||
|
|
JSON.stringify(patch.paramsSchema ?? existing.params_schema),
|
||
|
|
JSON.stringify(patch.defaultParams ?? existing.default_params)
|
||
|
|
]);
|
||
|
|
return normalizeInitialConditionGenerator(row);
|
||
|
|
}
|
||
|
|
export async function createCaEngine(db, input) {
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
INSERT INTO ca_engines (
|
||
|
|
slug,
|
||
|
|
name,
|
||
|
|
description,
|
||
|
|
engine_kind,
|
||
|
|
license,
|
||
|
|
owner,
|
||
|
|
ip_notice,
|
||
|
|
supported_classes,
|
||
|
|
params_schema,
|
||
|
|
default_params
|
||
|
|
)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8::jsonb, $9::jsonb, $10::jsonb)
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
input.slug,
|
||
|
|
input.name,
|
||
|
|
input.description ?? null,
|
||
|
|
input.engineKind,
|
||
|
|
input.license ?? null,
|
||
|
|
input.owner ?? null,
|
||
|
|
input.ipNotice ?? null,
|
||
|
|
JSON.stringify(input.supportedClasses),
|
||
|
|
JSON.stringify(input.paramsSchema ?? {}),
|
||
|
|
JSON.stringify(input.defaultParams ?? {})
|
||
|
|
]);
|
||
|
|
return normalizeCaEngine(row);
|
||
|
|
}
|
||
|
|
export async function listCaEngines(db, filter = {}) {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT *
|
||
|
|
FROM ca_engines
|
||
|
|
WHERE archived_at IS NULL
|
||
|
|
ORDER BY name, slug
|
||
|
|
`);
|
||
|
|
const engines = result.rows.map(normalizeCaEngine);
|
||
|
|
if (!filter.caClass)
|
||
|
|
return engines;
|
||
|
|
return engines.filter((engine) => engine.supported_classes.some((supportedClass) => caClassEquals(supportedClass, filter.caClass)));
|
||
|
|
}
|
||
|
|
export async function getCaEngine(db, engineId) {
|
||
|
|
const row = await queryOne(db, 'SELECT * FROM ca_engines WHERE id = $1', [engineId]);
|
||
|
|
return row ? normalizeCaEngine(row) : null;
|
||
|
|
}
|
||
|
|
export async function listCaEnginePresetUsage(db, engineId) {
|
||
|
|
const engine = await getCaEngine(db, engineId);
|
||
|
|
if (!engine)
|
||
|
|
return null;
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT nodes.*, trees.name AS tree_name
|
||
|
|
FROM ca_preset_nodes nodes
|
||
|
|
JOIN ca_preset_trees trees ON trees.id = nodes.tree_id
|
||
|
|
WHERE nodes.archived_at IS NULL
|
||
|
|
AND trees.archived_at IS NULL
|
||
|
|
ORDER BY trees.name, nodes.sort_order, nodes.name
|
||
|
|
`);
|
||
|
|
const nodes = result.rows.map((row) => ({ ...normalizePresetNode(row), tree_name: row.tree_name }));
|
||
|
|
const byId = new Map(nodes.map((node) => [`${node.tree_id}:${node.id}`, node]));
|
||
|
|
const resolvedById = new Map();
|
||
|
|
const resolving = new Set();
|
||
|
|
function resolveNode(node) {
|
||
|
|
const key = `${node.tree_id}:${node.id}`;
|
||
|
|
const cached = resolvedById.get(key);
|
||
|
|
if (cached)
|
||
|
|
return cached;
|
||
|
|
if (resolving.has(key))
|
||
|
|
return node.params;
|
||
|
|
resolving.add(key);
|
||
|
|
const parent = node.parent_id ? byId.get(`${node.tree_id}:${node.parent_id}`) : undefined;
|
||
|
|
const resolved = mergeSceneParams(parent ? resolveNode(parent) : {}, node.params);
|
||
|
|
resolving.delete(key);
|
||
|
|
resolvedById.set(key, resolved);
|
||
|
|
return resolved;
|
||
|
|
}
|
||
|
|
return nodes
|
||
|
|
.filter((node) => {
|
||
|
|
const simulation = resolveNode(node).simulation;
|
||
|
|
return isPlainObject(simulation) && simulation.engineId === engine.engine_kind;
|
||
|
|
})
|
||
|
|
.map((node) => ({
|
||
|
|
tree_id: node.tree_id,
|
||
|
|
tree_name: node.tree_name,
|
||
|
|
node_id: node.id,
|
||
|
|
node_name: node.name,
|
||
|
|
node_kind: node.kind
|
||
|
|
}));
|
||
|
|
}
|
||
|
|
export async function updateCaEngine(db, engineId, patch) {
|
||
|
|
const existing = await getCaEngine(db, engineId);
|
||
|
|
if (!existing)
|
||
|
|
return null;
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
UPDATE ca_engines
|
||
|
|
SET
|
||
|
|
slug = $2,
|
||
|
|
name = $3,
|
||
|
|
description = $4,
|
||
|
|
engine_kind = $5,
|
||
|
|
license = $6,
|
||
|
|
owner = $7,
|
||
|
|
ip_notice = $8,
|
||
|
|
supported_classes = $9::jsonb,
|
||
|
|
params_schema = $10::jsonb,
|
||
|
|
default_params = $11::jsonb,
|
||
|
|
updated_at = now()
|
||
|
|
WHERE id = $1
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
engineId,
|
||
|
|
patch.slug ?? existing.slug,
|
||
|
|
patch.name ?? existing.name,
|
||
|
|
Object.hasOwn(patch, 'description') ? patch.description : existing.description,
|
||
|
|
patch.engineKind ?? existing.engine_kind,
|
||
|
|
Object.hasOwn(patch, 'license') ? patch.license : existing.license,
|
||
|
|
Object.hasOwn(patch, 'owner') ? patch.owner : existing.owner,
|
||
|
|
Object.hasOwn(patch, 'ipNotice') ? patch.ipNotice : existing.ip_notice,
|
||
|
|
JSON.stringify(patch.supportedClasses ?? existing.supported_classes),
|
||
|
|
JSON.stringify(patch.paramsSchema ?? existing.params_schema),
|
||
|
|
JSON.stringify(patch.defaultParams ?? existing.default_params)
|
||
|
|
]);
|
||
|
|
return normalizeCaEngine(row);
|
||
|
|
}
|
||
|
|
export async function createDeck(db, input) {
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
INSERT INTO ca_decks (slug, title, description, schema_version, params)
|
||
|
|
VALUES ($1, $2, $3, $4, $5::jsonb)
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
input.slug,
|
||
|
|
input.title,
|
||
|
|
input.description ?? null,
|
||
|
|
input.schemaVersion ?? 1,
|
||
|
|
JSON.stringify(input.params ?? {})
|
||
|
|
]);
|
||
|
|
return normalizeDeck(row);
|
||
|
|
}
|
||
|
|
export async function listDecks(db) {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT *
|
||
|
|
FROM ca_decks
|
||
|
|
WHERE archived_at IS NULL
|
||
|
|
ORDER BY created_at, slug
|
||
|
|
`);
|
||
|
|
return result.rows.map(normalizeDeck);
|
||
|
|
}
|
||
|
|
export async function getDeck(db, deckId) {
|
||
|
|
const row = await queryOne(db, 'SELECT * FROM ca_decks WHERE id = $1', [deckId]);
|
||
|
|
return row ? normalizeDeck(row) : null;
|
||
|
|
}
|
||
|
|
export async function updateDeck(db, deckId, patch) {
|
||
|
|
const existing = await getDeck(db, deckId);
|
||
|
|
if (!existing)
|
||
|
|
return null;
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
UPDATE ca_decks
|
||
|
|
SET
|
||
|
|
slug = $2,
|
||
|
|
title = $3,
|
||
|
|
description = $4,
|
||
|
|
schema_version = $5,
|
||
|
|
params = $6::jsonb,
|
||
|
|
archived_at = $7,
|
||
|
|
updated_at = now()
|
||
|
|
WHERE id = $1
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
deckId,
|
||
|
|
patch.slug ?? existing.slug,
|
||
|
|
patch.title ?? existing.title,
|
||
|
|
Object.hasOwn(patch, 'description') ? patch.description : existing.description,
|
||
|
|
patch.schemaVersion ?? existing.schema_version,
|
||
|
|
JSON.stringify(patch.params ?? existing.params),
|
||
|
|
Object.hasOwn(patch, 'archivedAt') ? patch.archivedAt : existing.archived_at
|
||
|
|
]);
|
||
|
|
return normalizeDeck(row);
|
||
|
|
}
|
||
|
|
export async function deleteDeck(db, deckId) {
|
||
|
|
const result = await db.query('DELETE FROM ca_decks WHERE id = $1 RETURNING id', [deckId]);
|
||
|
|
return result.rows.length > 0;
|
||
|
|
}
|
||
|
|
export async function createScene(db, input) {
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
INSERT INTO ca_scenes (
|
||
|
|
deck_id,
|
||
|
|
order_index,
|
||
|
|
title,
|
||
|
|
description,
|
||
|
|
preset_tree_id,
|
||
|
|
preset_node_id,
|
||
|
|
apply_mode,
|
||
|
|
requires_previous_scene,
|
||
|
|
schema_version,
|
||
|
|
params,
|
||
|
|
transition
|
||
|
|
)
|
||
|
|
VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10::jsonb, $11::jsonb)
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
input.deckId,
|
||
|
|
input.orderIndex,
|
||
|
|
input.title,
|
||
|
|
input.description ?? null,
|
||
|
|
input.presetTreeId ?? null,
|
||
|
|
input.presetNodeId ?? null,
|
||
|
|
input.applyMode ?? 'reinitialize',
|
||
|
|
input.requiresPreviousScene ?? input.applyMode === 'patch_existing',
|
||
|
|
input.schemaVersion ?? 1,
|
||
|
|
JSON.stringify(input.params ?? {}),
|
||
|
|
JSON.stringify(input.transition ?? {})
|
||
|
|
]);
|
||
|
|
return normalizeScene(row);
|
||
|
|
}
|
||
|
|
export async function getScene(db, sceneId) {
|
||
|
|
const row = await queryOne(db, 'SELECT * FROM ca_scenes WHERE id = $1', [sceneId]);
|
||
|
|
return row ? normalizeScene(row) : null;
|
||
|
|
}
|
||
|
|
export async function listScenes(db, deckId) {
|
||
|
|
const result = await db.query(`
|
||
|
|
SELECT *
|
||
|
|
FROM ca_scenes
|
||
|
|
WHERE deck_id = $1
|
||
|
|
ORDER BY order_index
|
||
|
|
`, [deckId]);
|
||
|
|
return result.rows.map(normalizeScene);
|
||
|
|
}
|
||
|
|
export async function updateScene(db, sceneId, patch) {
|
||
|
|
const existing = await getScene(db, sceneId);
|
||
|
|
if (!existing)
|
||
|
|
return null;
|
||
|
|
const nextApplyMode = patch.applyMode ?? existing.apply_mode;
|
||
|
|
const nextRequiresPreviousScene = Object.hasOwn(patch, 'requiresPreviousScene')
|
||
|
|
? patch.requiresPreviousScene
|
||
|
|
: nextApplyMode === 'patch_existing'
|
||
|
|
? true
|
||
|
|
: existing.requires_previous_scene;
|
||
|
|
const row = await queryRequired(db, `
|
||
|
|
UPDATE ca_scenes
|
||
|
|
SET
|
||
|
|
deck_id = $2,
|
||
|
|
order_index = $3,
|
||
|
|
title = $4,
|
||
|
|
description = $5,
|
||
|
|
preset_tree_id = $6,
|
||
|
|
preset_node_id = $7,
|
||
|
|
apply_mode = $8,
|
||
|
|
requires_previous_scene = $9,
|
||
|
|
schema_version = $10,
|
||
|
|
params = $11::jsonb,
|
||
|
|
transition = $12::jsonb,
|
||
|
|
archived_at = $13,
|
||
|
|
updated_at = now()
|
||
|
|
WHERE id = $1
|
||
|
|
RETURNING *
|
||
|
|
`, [
|
||
|
|
sceneId,
|
||
|
|
patch.deckId ?? existing.deck_id,
|
||
|
|
patch.orderIndex ?? existing.order_index,
|
||
|
|
patch.title ?? existing.title,
|
||
|
|
Object.hasOwn(patch, 'description') ? patch.description : existing.description,
|
||
|
|
Object.hasOwn(patch, 'presetTreeId') ? patch.presetTreeId : existing.preset_tree_id,
|
||
|
|
Object.hasOwn(patch, 'presetNodeId') ? patch.presetNodeId : existing.preset_node_id,
|
||
|
|
nextApplyMode,
|
||
|
|
nextRequiresPreviousScene,
|
||
|
|
patch.schemaVersion ?? existing.schema_version,
|
||
|
|
JSON.stringify(patch.params ?? existing.params),
|
||
|
|
JSON.stringify(patch.transition ?? existing.transition),
|
||
|
|
Object.hasOwn(patch, 'archivedAt') ? patch.archivedAt : existing.archived_at
|
||
|
|
]);
|
||
|
|
return normalizeScene(row);
|
||
|
|
}
|
||
|
|
export async function deleteScene(db, sceneId) {
|
||
|
|
const result = await db.query('DELETE FROM ca_scenes WHERE id = $1 RETURNING id', [sceneId]);
|
||
|
|
return result.rows.length > 0;
|
||
|
|
}
|
||
|
|
export async function resolveScene(db, sceneId) {
|
||
|
|
const sceneRow = await queryOne(db, 'SELECT * FROM ca_scenes WHERE id = $1', [sceneId]);
|
||
|
|
if (!sceneRow)
|
||
|
|
return null;
|
||
|
|
const scene = normalizeScene(sceneRow);
|
||
|
|
if (!scene.preset_tree_id || !scene.preset_node_id) {
|
||
|
|
return {
|
||
|
|
scene,
|
||
|
|
ancestry: [],
|
||
|
|
params: scene.params
|
||
|
|
};
|
||
|
|
}
|
||
|
|
const resolvedNode = await resolvePresetNode(db, scene.preset_tree_id, scene.preset_node_id);
|
||
|
|
if (!resolvedNode)
|
||
|
|
return null;
|
||
|
|
return {
|
||
|
|
scene,
|
||
|
|
ancestry: resolvedNode.ancestry,
|
||
|
|
params: mergeSceneParams(resolvedNode.params, scene.params)
|
||
|
|
};
|
||
|
|
}
|
||
|
|
export async function resolveDeck(db, deckId) {
|
||
|
|
const deck = await getDeck(db, deckId);
|
||
|
|
if (!deck)
|
||
|
|
return null;
|
||
|
|
const scenes = await listScenes(db, deckId);
|
||
|
|
const resolvedScenes = [];
|
||
|
|
for (const scene of scenes) {
|
||
|
|
if (!scene.preset_tree_id || !scene.preset_node_id) {
|
||
|
|
resolvedScenes.push({
|
||
|
|
scene,
|
||
|
|
ancestry: [],
|
||
|
|
params: scene.params
|
||
|
|
});
|
||
|
|
continue;
|
||
|
|
}
|
||
|
|
const resolvedNode = await resolvePresetNode(db, scene.preset_tree_id, scene.preset_node_id);
|
||
|
|
if (!resolvedNode) {
|
||
|
|
throw new Error(`Unable to resolve preset node for scene ${scene.id}`);
|
||
|
|
}
|
||
|
|
resolvedScenes.push({
|
||
|
|
scene,
|
||
|
|
ancestry: resolvedNode.ancestry,
|
||
|
|
params: mergeSceneParams(resolvedNode.params, scene.params)
|
||
|
|
});
|
||
|
|
}
|
||
|
|
return {
|
||
|
|
deck,
|
||
|
|
scenes: resolvedScenes
|
||
|
|
};
|
||
|
|
}
|
||
|
|
//# sourceMappingURL=caStudioRepository.js.map
|