feat(biochem): Фаза 4 (4.1-4.3) — пути метаболизма из БД (API), хардкод убран

Перенос данных путей из ~700 строк инлайн-объекта PATHWAYS в biochem-pathways.html
в БД. Document-подход: каждый путь — самодостаточный документ data_json (граф
узлов/рёбер + шаги с квизами); путь всегда читается целиком, реляционных
запросов нет — нормализация не нужна.

- migration 045_bio_pathways: таблица bio_pathways(slug, name, color, ord, data_json).
- backend/scripts/biochem_pathways_data.js: данные 4 путей (извлечены из инлайн-
  объекта, теперь самодостаточный источник правды).
- seed_biochem_pathways.js: идемпотентный upsert по slug.
- biochemController.getPathways + GET /biochem/pathways (карта slug->данные).
- js/api.js: biochemGetPathways.
- biochem-pathways.html: инлайн PATHWAYS (-238 строк) заменён на загрузку из API
  в init (loadPathways); форма данных идентична — рендер не изменён.

Проверено: API отдаёт 4 пути в форме фронта, сидер идемпотентен.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Maxim Dolgolyov
2026-05-30 17:39:36 +03:00
parent e2ff28a482
commit b29b395a96
7 changed files with 1372 additions and 240 deletions
File diff suppressed because it is too large Load Diff
+42
View File
@@ -0,0 +1,42 @@
'use strict';
/*
* Сид метаболических путей в bio_pathways.
* Источник данных — backend/scripts/data/biochem_pathways.json (изначально
* извлечён из инлайн-объекта PATHWAYS; теперь это самодостаточный источник
* правды, не зависящий от фронта). Каждый путь — документ data_json.
* Идемпотентно (upsert по slug): повторный запуск синхронизирует данные.
*
* Запуск: node backend/scripts/seed_biochem_pathways.js
*/
const db = require('../src/db/db');
const P = require('./biochem_pathways_data');
const upsert = db.prepare(`INSERT INTO bio_pathways (slug, name, color, ord, data_json)
VALUES (@slug, @name, @color, @ord, @data_json)
ON CONFLICT(slug) DO UPDATE SET
name=excluded.name, color=excluded.color, ord=excluded.ord, data_json=excluded.data_json`);
const slugs = Object.keys(P);
let n = 0;
db.exec('BEGIN');
try {
slugs.forEach((slug, idx) => {
const p = P[slug];
upsert.run({
slug,
name: p.name || slug,
color: p.color || '#9B5DE5',
ord: idx,
data_json: JSON.stringify(p),
});
n++;
});
db.exec('COMMIT');
} catch (e) {
db.exec('ROLLBACK');
throw e;
}
console.log(`biochem pathways seed: ${n} путь(ей) — ${slugs.join(', ')}`);
console.log('в БД:', db.prepare('SELECT slug, name, LENGTH(data_json) AS bytes FROM bio_pathways ORDER BY ord').all()
.map(r => `${r.slug}(${r.bytes}b)`).join(' '));
+9 -1
View File
@@ -321,6 +321,14 @@ function deleteSaved(req, res) {
res.json({ ok: true });
}
/* ── GET /api/biochem/pathways — все пути из БД (карта slug → данные) ──── */
const stmtsPathways = db.prepare('SELECT slug, data_json FROM bio_pathways ORDER BY ord');
function getPathways(_req, res) {
const out = {};
for (const r of stmtsPathways.all()) out[r.slug] = tryParse(r.data_json, {});
res.json(out);
}
/* ── Прогресс прохождения путей (Learn-режим) ────────────────────────── */
const PATHWAY_XP = 80;
const stmtsPath = {
@@ -369,7 +377,7 @@ module.exports = {
getElements, getMolecules, getMolecule, validate,
getReactions, getChallenges, solveChallenge,
getSaved, saveMolecule, deleteSaved,
getPathwayProgress, savePathwayProgress,
getPathways, getPathwayProgress, savePathwayProgress,
// экспортируется для тестов структурной проверки
structuralMatch, canonicalHash,
};
@@ -0,0 +1,15 @@
-- 045_bio_pathways.sql
-- Метаболические пути как данные (вместо ~700 строк хардкода в
-- biochem-pathways.html). Каждый путь — самодостаточный документ (граф узлов
-- и рёбер + шаги Learn-режима с квизами) в data_json; страница грузит их через
-- API. Document-подход выбран намеренно: путь всегда читается целиком,
-- реляционных запросов к узлам/рёбрам нет.
CREATE TABLE IF NOT EXISTS bio_pathways (
id INTEGER PRIMARY KEY AUTOINCREMENT,
slug TEXT NOT NULL UNIQUE,
name TEXT NOT NULL,
color TEXT NOT NULL DEFAULT '#9B5DE5',
ord INTEGER NOT NULL DEFAULT 0,
data_json TEXT NOT NULL DEFAULT '{}'
);
+1
View File
@@ -14,6 +14,7 @@ router.post('/challenges/:id/solve', c.solveChallenge);
router.get('/saved', c.getSaved);
router.post('/saved', c.saveMolecule);
router.delete('/saved/:id', c.deleteSaved);
router.get('/pathways', c.getPathways);
router.get('/pathways/progress', c.getPathwayProgress);
router.post('/pathways/progress', c.savePathwayProgress);