Historique des versions — frise + rédaction
Table NocoDB Versions (Type, RecordId, Snapshot, Label). Snapshots auto avant chaque modification de fiche frise et de texte rédigé. Archive avant suppression de fiche. Panneau latéral avec aperçu et restauration. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -18,6 +18,27 @@ const NOCODB_VISION_TABLE_ID = process.env.NOCODB_VISION_TABLE_ID || '';
|
||||
const NOCODB_CONTENU_TABLE_ID = process.env.NOCODB_CONTENU_TABLE_ID || '';
|
||||
const NOCODB_PERSONNAGES_TABLE_ID = process.env.NOCODB_PERSONNAGES_TABLE_ID || '';
|
||||
const NOCODB_RESSOURCES_TABLE_ID = process.env.NOCODB_RESSOURCES_TABLE_ID || '';
|
||||
const NOCODB_VERSIONS_TABLE_ID = process.env.NOCODB_VERSIONS_TABLE_ID || '';
|
||||
|
||||
// ── Version history helper ──
|
||||
async function saveVersion(type, recordId, snapshot, label) {
|
||||
if (!NOCODB_VERSIONS_TABLE_ID) return;
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}`;
|
||||
await fetch(url, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
Type: type,
|
||||
RecordId: recordId,
|
||||
Snapshot: JSON.stringify(snapshot),
|
||||
Label: label || '',
|
||||
}),
|
||||
});
|
||||
} catch (e) {
|
||||
console.error('saveVersion error:', e.message);
|
||||
}
|
||||
}
|
||||
|
||||
// Auth
|
||||
function makeToken(ts) {
|
||||
@@ -77,10 +98,19 @@ app.post('/api/records', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy NocoDB — update record
|
||||
// Proxy NocoDB — update record (with version snapshot)
|
||||
app.patch('/api/records/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${req.params.id}`;
|
||||
const recordId = Number(req.params.id);
|
||||
// Snapshot current state before update
|
||||
const getUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||
const getR = await fetch(getUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
if (getR.ok) {
|
||||
const current = await getR.json();
|
||||
await saveVersion('frise', recordId, current, current.Titre || '');
|
||||
}
|
||||
// Proceed with update
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
@@ -93,10 +123,18 @@ app.patch('/api/records/:id', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// Proxy NocoDB — delete record
|
||||
// Proxy NocoDB — delete record (archive before deletion)
|
||||
app.delete('/api/records/:id', requireAuth, async (req, res) => {
|
||||
try {
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${req.params.id}`;
|
||||
const recordId = Number(req.params.id);
|
||||
// Archive before delete
|
||||
const getUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||
const getR = await fetch(getUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
if (getR.ok) {
|
||||
const current = await getR.json();
|
||||
await saveVersion('frise', recordId, current, `[supprimé] ${current.Titre || ''}`);
|
||||
}
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${recordId}`;
|
||||
const r = await fetch(url, {
|
||||
method: 'DELETE',
|
||||
headers: { 'xc-token': NOCODB_TOKEN },
|
||||
@@ -243,6 +281,8 @@ app.put('/api/contenu/:sceneId', requireAuth, async (req, res) => {
|
||||
const payload = { SceneId: sceneId, Texte: req.body.Texte, Mots: req.body.Mots || 0 };
|
||||
|
||||
if (existing) {
|
||||
// Snapshot before update
|
||||
await saveVersion('redaction', sceneId, { Texte: existing.Texte, Mots: existing.Mots, SceneId: sceneId }, `Scène #${sceneId}`);
|
||||
// Update
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}/${existing.Id}`;
|
||||
const r = await fetch(url, {
|
||||
@@ -268,6 +308,83 @@ app.put('/api/contenu/:sceneId', requireAuth, async (req, res) => {
|
||||
}
|
||||
});
|
||||
|
||||
// ── Versions API ──
|
||||
|
||||
// List versions for a record
|
||||
app.get('/api/versions/:type/:recordId', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_VERSIONS_TABLE_ID) return res.json({ list: [] });
|
||||
try {
|
||||
const { type, recordId } = req.params;
|
||||
const url = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}?where=(Type,eq,${type})~and(RecordId,eq,${recordId})&sort=-CreatedAt&limit=50`;
|
||||
const r = await fetch(url, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const data = await r.json();
|
||||
res.json(data);
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// Restore a frise version
|
||||
app.post('/api/versions/:id/restore', requireAuth, async (req, res) => {
|
||||
if (!NOCODB_VERSIONS_TABLE_ID) return res.status(400).json({ error: 'Versions non configurées' });
|
||||
try {
|
||||
// Get the version
|
||||
const vUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_VERSIONS_TABLE_ID}/${req.params.id}`;
|
||||
const vR = await fetch(vUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
if (!vR.ok) return res.status(404).json({ error: 'Version introuvable' });
|
||||
const version = await vR.json();
|
||||
const snapshot = JSON.parse(version.Snapshot);
|
||||
|
||||
if (version.Type === 'frise') {
|
||||
// Snapshot current before restoring
|
||||
const getUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${version.RecordId}`;
|
||||
const getR = await fetch(getUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
if (getR.ok) {
|
||||
const current = await getR.json();
|
||||
await saveVersion('frise', version.RecordId, current, `[avant restauration] ${current.Titre || ''}`);
|
||||
}
|
||||
// Restore — only user-editable fields
|
||||
const { Titre, Resume, Personnage, Tags, Notes, Acte, Ordre } = snapshot;
|
||||
const patchUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_TABLE_ID}/${version.RecordId}`;
|
||||
const r = await fetch(patchUrl, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ Titre, Resume, Personnage, Tags, Notes, Acte, Ordre }),
|
||||
});
|
||||
res.json(await r.json());
|
||||
} else if (version.Type === 'redaction') {
|
||||
// Snapshot current text before restoring
|
||||
const findUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}?where=(SceneId,eq,${version.RecordId})&limit=1`;
|
||||
const findR = await fetch(findUrl, { headers: { 'xc-token': NOCODB_TOKEN } });
|
||||
const findData = await findR.json();
|
||||
const existing = findData.list?.[0];
|
||||
if (existing) {
|
||||
await saveVersion('redaction', version.RecordId, { Texte: existing.Texte, Mots: existing.Mots, SceneId: version.RecordId }, `[avant restauration] Scène #${version.RecordId}`);
|
||||
const patchUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}/${existing.Id}`;
|
||||
const r = await fetch(patchUrl, {
|
||||
method: 'PATCH',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ Texte: snapshot.Texte, Mots: snapshot.Mots || 0 }),
|
||||
});
|
||||
res.json(await r.json());
|
||||
} else {
|
||||
// Re-create content row
|
||||
const createUrl = `${NOCODB_URL}/api/v1/db/data/noco/${NOCODB_BASE_ID}/${NOCODB_CONTENU_TABLE_ID}`;
|
||||
const r = await fetch(createUrl, {
|
||||
method: 'POST',
|
||||
headers: { 'xc-token': NOCODB_TOKEN, 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ SceneId: version.RecordId, Texte: snapshot.Texte, Mots: snapshot.Mots || 0 }),
|
||||
});
|
||||
res.json(await r.json());
|
||||
}
|
||||
} else {
|
||||
res.status(400).json({ error: 'Type inconnu' });
|
||||
}
|
||||
} catch (e) {
|
||||
res.status(500).json({ error: e.message });
|
||||
}
|
||||
});
|
||||
|
||||
// ── Personnages API ──
|
||||
|
||||
function personnagesProxy(tableIdVar) {
|
||||
|
||||
Reference in New Issue
Block a user