openvibers/openvibe.live Commits
Frozen legacy tables: drop the ten uncalled write helpers for vods/clips/pastes/paste_likes and add test/frozen-tables.test.js, which fails if server code writes them again (Wave 22 register C-73 step 1)
Changes
@@@ -3204,10 +3204,6 @@ function getPastesNeedingAnalysis(limit = 5) {return all("SELECT * FROM pastes WHERE ai_summary IS NULL AND type IN ('paste','screenshot') ORDER BY created_at DESC LIMIT ?", [limit]);}// deleteAiMomentTextPastes() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.--function updatePasteAi(pasteId, { ai_summary = null, ai_tags = null }) {-- return run('UPDATE pastes SET ai_summary = ?, ai_tags = ?, ai_analyzed_at = CURRENT_TIMESTAMP WHERE id = ?',-- [ai_summary, ai_tags ? (typeof ai_tags === 'string' ? ai_tags : JSON.stringify(ai_tags)) : null, pasteId]);--}// ── One-time cleanup: earlier builds stored raw (often malformed) model JSON like// `{"description":"…","tags":[…]}` directly into text columns. Extract just the// human description so cards/overviews stop showing JSON. Idempotent; cheap to re-run.@@@ -5560,52 +5556,10 @@ function setSubscriptionStatus(id, status, fields = {}) {[status, status === 'active' ? 1 : 0, cape, cpe, id]);return get('SELECT * FROM subscriptions WHERE id = ?', [id]);}--// ── VOD helpers ──────────────────────────────────────────────// createVod() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.----function updateVodHealth(vodId, { status, score, issues = [], probeDuration, probeFormat, quarantine = false, keepPublic = false }) {-- const updates = [];-- const params = [];-- if (status) {-- updates.push('health_status = ?');-- params.push(status);-- }-- if (typeof score === 'number') {-- updates.push('health_score = ?');-- params.push(score);-- }-- if (issues) {-- updates.push('health_issues_json = ?');-- params.push(JSON.stringify(issues));-- }-- if (typeof probeDuration === 'number') {-- updates.push('probe_duration_seconds = ?');-- params.push(probeDuration);-- }-- if (probeFormat !== undefined) {-- updates.push('probe_format_json = ?');-- params.push(JSON.stringify(probeFormat || {}));-- }-- if (quarantine) {-- updates.push('quarantined_at = datetime(\'now\')');-- if (!keepPublic) {-- updates.push('is_public = 0');-- }-- }-- updates.push('last_health_scan_at = datetime(\'now\')');-- params.push(vodId);-- if (!updates.length) return null;-- return run(`UPDATE vods SET ${updates.join(', ')} WHERE id = ?`, params);--}----function repairVodDuration(vodId, duration, fileSize) {-- return run(-- `UPDATE vods SET duration_seconds = ?, file_size = ?, probe_duration_seconds = ?, last_health_scan_at = datetime('now') WHERE id = ?`,-- [duration, fileSize, duration, vodId]-- );--}++// The legacy vods/clips/pastes tables are read-only here: test/frozen-tables.test.js fails on a new write.function getVodHealthById(id) {return get(`SELECT * FROM vods WHERE id = ?`, [id]);@@@ -7290,29 +7244,6 @@ function listPastes({ visibility = 'public', type, search, limit = 30, offset =return { pastes, total };}--function incrementPasteViews(slug) {-- return run('UPDATE pastes SET views = views + 1 WHERE slug = ?', [slug]);--}----function updatePaste(slug, fields) {-- const updates = [];-- const params = [];-- for (const [key, val] of Object.entries(fields)) {-- if (['title', 'content', 'language', 'visibility', 'pinned', 'metadata'].includes(key)) {-- updates.push(`${key} = ?`);-- params.push(val);-- }-- }-- if (updates.length === 0) return;-- updates.push('updated_at = CURRENT_TIMESTAMP');-- params.push(slug);-- return run(`UPDATE pastes SET ${updates.join(', ')} WHERE slug = ?`, params);--}----function deletePaste(slug) {-- return run('DELETE FROM pastes WHERE slug = ?', [slug]);--}--function getUserPastes(userId, limit = 50) {return all(`SELECT id, slug, type, title, language, visibility, burn_after_read, pinned, views, copies, likes, created_at@@@ -7320,27 +7251,11 @@ function getUserPastes(userId, limit = 50) {`, [userId, limit]);}--function likePaste(pasteId, userId) {-- run('INSERT OR IGNORE INTO paste_likes (paste_id, user_id) VALUES (?, ?)', [pasteId, userId]);-- run('UPDATE pastes SET likes = (SELECT COUNT(*) FROM paste_likes WHERE paste_id = ?) WHERE id = ?', [pasteId, pasteId]);-- return get('SELECT likes FROM pastes WHERE id = ?', [pasteId]);--}----function unlikePaste(pasteId, userId) {-- run('DELETE FROM paste_likes WHERE paste_id = ? AND user_id = ?', [pasteId, userId]);-- run('UPDATE pastes SET likes = (SELECT COUNT(*) FROM paste_likes WHERE paste_id = ?) WHERE id = ?', [pasteId, pasteId]);-- return get('SELECT likes FROM pastes WHERE id = ?', [pasteId]);--}--function hasUserLikedPaste(pasteId, userId) {const row = get('SELECT 1 FROM paste_likes WHERE paste_id = ? AND user_id = ?', [pasteId, userId]);return !!row;}--function incrementPasteCopies(slug) {-- return run('UPDATE pastes SET copies = copies + 1 WHERE slug = ?', [slug]);--}--function countUserPastesToday(userId, ip) {if (userId) {return get("SELECT COUNT(*) as c FROM pastes WHERE user_id = ? AND created_at > datetime('now', '-1 day')", [userId])?.c || 0;@@@ -7380,16 +7295,6 @@ function getLastPasteTime(userId, ip) {return row ? new Date(row.created_at + (row.created_at.includes('Z') ? '' : 'Z')).getTime() : 0;}--function deleteAllForks() {-- const forks = all('SELECT id, screenshot_path FROM pastes WHERE forked_from IS NOT NULL');-- // Unlink the fork screenshots so they don't leak on disk.-- for (const f of forks) {-- if (f.screenshot_path) { try { fs.unlinkSync(f.screenshot_path); } catch { /* ignore */ } }-- }-- run('DELETE FROM pastes WHERE forked_from IS NOT NULL');-- return forks.length;--}--function getPasteStats() {const total = get('SELECT COUNT(*) as c FROM pastes')?.c || 0;const textPastes = get("SELECT COUNT(*) as c FROM pastes WHERE type = 'paste'")?.c || 0;@@@ -8437,7 +8342,7 @@ module.exports = {setVodAiOverview, setClipAiOverview, setVodTranscript, setClipTranscript, getStreamMemoriesInRange,getVodsNeedingOverview, getClipsNeedingOverview, getVodsNeedingTimeline, getVodsNeedingTranscript, getClipsNeedingTranscript, getPastesNeedingAnalysis,setVodTranscriptStatus, setClipTranscriptStatus, bumpVodTranscriptAttempt, bumpClipTranscriptAttempt,-- updatePasteAi, cleanupMalformedAiText, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,++ cleanupMalformedAiText, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,getStreamMemoriesByUser, countStreamMemoriesByUser, getAiMomentCandidates, getStreamTranscriptSegments, getUserPastesForAi,addTimelineEvents, getTimeline, getTimelineText, getTimelineCoverage, linkTimelineToVod, getTimelineByVod, getTimelineVodId,getVodsForMomentRanking, getClipStartTimesForStream, getChatSpikeOffsets,@@@ -8508,7 +8413,7 @@ module.exports = {renormalizePendingMediaRequestPositions,// VODsgetVodById, getVodsByUser, countVodsByUser, getPublicVods, countPublicVods, listVodStreamers, getActiveVodByStream,-- updateVodHealth, repairVodDuration, getVodHealthById, getVodScanCandidates,++ getVodHealthById, getVodScanCandidates,getVodsNeedingHealthScan, getQuarantinedVodsForCleanup,// ClipsgetClipById, getClipsByUser, countClipsByUser, getPublicClips, countPublicClips, listClipStreamers, getClipsByStream, getClipsOfUserStreams,@@@ -8558,9 +8463,8 @@ module.exports = {getChannelModerationSettings, upsertChannelModerationSettings,// PastesgetPasteBySlug, getPasteById, listPastes,-- incrementPasteViews, updatePaste, deletePaste, getUserPastes, getUserPastesForChannel, countUserPastesForChannel,-- likePaste, unlikePaste, hasUserLikedPaste, incrementPasteCopies,-- countUserPastesToday, getLastPasteTime, deleteAllForks, getPasteStats, getUserTotalGameLevel,++ getUserPastes, getUserPastesForChannel, countUserPastesForChannel,++ hasUserLikedPaste, countUserPastesToday, getLastPasteTime, getPasteStats, getUserTotalGameLevel,// Paste CommentscreatePasteComment, getPasteComments, getPasteCommentReplies,getPasteCommentById, getPasteCommentCount, deletePasteComment,nnew file mode 100644@@@ -0,0 +1,37 @@++'use strict';++++// Live's legacy vods/clips/pastes tables are frozen: Media and Community own that data now++// (AGENTS.md, Wave 22 register C-73). Live may read them for pre-cutover pages, but no server code++// may write them. The only writes allowed are the two guarded one-time visibility backfills that++// run when the column is first added.++++const assert = require('assert');++const fs = require('fs');++const path = require('path');++++const ROOT = path.join(__dirname, '..');++const WRITE = /\b(INSERT(?:\s+OR\s+\w+)?\s+INTO|UPDATE|DELETE\s+FROM|REPLACE\s+INTO)\s+(vods|clips|pastes|paste_likes)\b/i;++const ALLOWED = [++ "UPDATE vods SET visibility = CASE WHEN is_public = 1 THEN 'public' ELSE 'private' END",++ "UPDATE clips SET visibility = CASE WHEN is_public = 1 THEN 'public' ELSE 'unlisted' END",++];++++function walk(dir, out = []) {++ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {++ const p = path.join(dir, e.name);++ if (e.isDirectory()) walk(p, out);++ else if (e.name.endsWith('.js')) out.push(p);++ }++ return out;++}++++const offenders = [];++for (const file of [...walk(path.join(ROOT, 'server')), ...walk(path.join(ROOT, 'scripts'))]) {++ fs.readFileSync(file, 'utf8').split('\n').forEach((line, i) => {++ if (!WRITE.test(line) || /^\s*(\/\/|\*)/.test(line)) return;++ if (ALLOWED.some((a) => line.includes(a))) return;++ offenders.push(`${path.relative(ROOT, file)}:${i + 1}: ${line.trim().slice(0, 120)}`);++ });++}++assert.deepStrictEqual(offenders, [], `writes to frozen tables:\n${offenders.join('\n')}`);++console.log('frozen tables: all checks passed');
@@ -3204,10 +3204,6 @@ function getPastesNeedingAnalysis(limit = 5) {
@@ -3204,10 +3204,6 @@ function getPastesNeedingAnalysis(limit = 5) {
return all("SELECT * FROM pastes WHERE ai_summary IS NULL AND type IN ('paste','screenshot') ORDER BY created_at DESC LIMIT ?", [limit]);
return all("SELECT * FROM pastes WHERE ai_summary IS NULL AND type IN ('paste','screenshot') ORDER BY created_at DESC LIMIT ?", [limit]);
}
}
// deleteAiMomentTextPastes() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.
// deleteAiMomentTextPastes() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.
-function updatePasteAi(pasteId, { ai_summary = null, ai_tags = null }) {
- return run('UPDATE pastes SET ai_summary = ?, ai_tags = ?, ai_analyzed_at = CURRENT_TIMESTAMP WHERE id = ?',
- [ai_summary, ai_tags ? (typeof ai_tags === 'string' ? ai_tags : JSON.stringify(ai_tags)) : null, pasteId]);
-}
// ── One-time cleanup: earlier builds stored raw (often malformed) model JSON like
// ── One-time cleanup: earlier builds stored raw (often malformed) model JSON like
// `{"description":"…","tags":[…]}` directly into text columns. Extract just the
// `{"description":"…","tags":[…]}` directly into text columns. Extract just the
// human description so cards/overviews stop showing JSON. Idempotent; cheap to re-run.
// human description so cards/overviews stop showing JSON. Idempotent; cheap to re-run.
@@ -5560,52 +5556,10 @@ function setSubscriptionStatus(id, status, fields = {}) {
@@ -5560,52 +5556,10 @@ function setSubscriptionStatus(id, status, fields = {}) {
[status, status === 'active' ? 1 : 0, cape, cpe, id]);
[status, status === 'active' ? 1 : 0, cape, cpe, id]);
return get('SELECT * FROM subscriptions WHERE id = ?', [id]);
return get('SELECT * FROM subscriptions WHERE id = ?', [id]);
}
}
-
// ── VOD helpers ──────────────────────────────────────────────
// ── VOD helpers ──────────────────────────────────────────────
// createVod() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.
// createVod() removed — the media subsystem (vods/clips/pastes writes) moved to OpenVibe.Media.
-
-function updateVodHealth(vodId, { status, score, issues = [], probeDuration, probeFormat, quarantine = false, keepPublic = false }) {
- const updates = [];
- const params = [];
- if (status) {
- updates.push('health_status = ?');
- params.push(status);
- }
- if (typeof score === 'number') {
- updates.push('health_score = ?');
- params.push(score);
- }
- if (issues) {
- updates.push('health_issues_json = ?');
- params.push(JSON.stringify(issues));
- }
- if (typeof probeDuration === 'number') {
- updates.push('probe_duration_seconds = ?');
- params.push(probeDuration);
- }
- if (probeFormat !== undefined) {
- updates.push('probe_format_json = ?');
- params.push(JSON.stringify(probeFormat || {}));
- }
- if (quarantine) {
- updates.push('quarantined_at = datetime(\'now\')');
- if (!keepPublic) {
- updates.push('is_public = 0');
- }
- }
- updates.push('last_health_scan_at = datetime(\'now\')');
- params.push(vodId);
- if (!updates.length) return null;
- return run(`UPDATE vods SET ${updates.join(', ')} WHERE id = ?`, params);
-}
-
-function repairVodDuration(vodId, duration, fileSize) {
- return run(
- `UPDATE vods SET duration_seconds = ?, file_size = ?, probe_duration_seconds = ?, last_health_scan_at = datetime('now') WHERE id = ?`,
- [duration, fileSize, duration, vodId]
- );
-}
+// The legacy vods/clips/pastes tables are read-only here: test/frozen-tables.test.js fails on a new write.
function getVodHealthById(id) {
function getVodHealthById(id) {
return get(`SELECT * FROM vods WHERE id = ?`, [id]);
return get(`SELECT * FROM vods WHERE id = ?`, [id]);
@@ -7290,29 +7244,6 @@ function listPastes({ visibility = 'public', type, search, limit = 30, offset =
@@ -7290,29 +7244,6 @@ function listPastes({ visibility = 'public', type, search, limit = 30, offset =
return { pastes, total };
return { pastes, total };
}
}
-function incrementPasteViews(slug) {
- return run('UPDATE pastes SET views = views + 1 WHERE slug = ?', [slug]);
-}
-
-function updatePaste(slug, fields) {
- const updates = [];
- const params = [];
- for (const [key, val] of Object.entries(fields)) {
- if (['title', 'content', 'language', 'visibility', 'pinned', 'metadata'].includes(key)) {
- updates.push(`${key} = ?`);
- params.push(val);
- }
- }
- if (updates.length === 0) return;
- updates.push('updated_at = CURRENT_TIMESTAMP');
- params.push(slug);
- return run(`UPDATE pastes SET ${updates.join(', ')} WHERE slug = ?`, params);
-}
-
-function deletePaste(slug) {
- return run('DELETE FROM pastes WHERE slug = ?', [slug]);
-}
-
function getUserPastes(userId, limit = 50) {
function getUserPastes(userId, limit = 50) {
return all(`
return all(`
SELECT id, slug, type, title, language, visibility, burn_after_read, pinned, views, copies, likes, created_at
SELECT id, slug, type, title, language, visibility, burn_after_read, pinned, views, copies, likes, created_at
@@ -7320,27 +7251,11 @@ function getUserPastes(userId, limit = 50) {
@@ -7320,27 +7251,11 @@ function getUserPastes(userId, limit = 50) {
`, [userId, limit]);
`, [userId, limit]);
}
}
-function likePaste(pasteId, userId) {
- run('INSERT OR IGNORE INTO paste_likes (paste_id, user_id) VALUES (?, ?)', [pasteId, userId]);
- run('UPDATE pastes SET likes = (SELECT COUNT(*) FROM paste_likes WHERE paste_id = ?) WHERE id = ?', [pasteId, pasteId]);
- return get('SELECT likes FROM pastes WHERE id = ?', [pasteId]);
-}
-
-function unlikePaste(pasteId, userId) {
- run('DELETE FROM paste_likes WHERE paste_id = ? AND user_id = ?', [pasteId, userId]);
- run('UPDATE pastes SET likes = (SELECT COUNT(*) FROM paste_likes WHERE paste_id = ?) WHERE id = ?', [pasteId, pasteId]);
- return get('SELECT likes FROM pastes WHERE id = ?', [pasteId]);
-}
-
function hasUserLikedPaste(pasteId, userId) {
function hasUserLikedPaste(pasteId, userId) {
const row = get('SELECT 1 FROM paste_likes WHERE paste_id = ? AND user_id = ?', [pasteId, userId]);
const row = get('SELECT 1 FROM paste_likes WHERE paste_id = ? AND user_id = ?', [pasteId, userId]);
return !!row;
return !!row;
}
}
-function incrementPasteCopies(slug) {
- return run('UPDATE pastes SET copies = copies + 1 WHERE slug = ?', [slug]);
-}
-
function countUserPastesToday(userId, ip) {
function countUserPastesToday(userId, ip) {
if (userId) {
if (userId) {
return get("SELECT COUNT(*) as c FROM pastes WHERE user_id = ? AND created_at > datetime('now', '-1 day')", [userId])?.c || 0;
return get("SELECT COUNT(*) as c FROM pastes WHERE user_id = ? AND created_at > datetime('now', '-1 day')", [userId])?.c || 0;
@@ -7380,16 +7295,6 @@ function getLastPasteTime(userId, ip) {
@@ -7380,16 +7295,6 @@ function getLastPasteTime(userId, ip) {
return row ? new Date(row.created_at + (row.created_at.includes('Z') ? '' : 'Z')).getTime() : 0;
return row ? new Date(row.created_at + (row.created_at.includes('Z') ? '' : 'Z')).getTime() : 0;
}
}
-function deleteAllForks() {
- const forks = all('SELECT id, screenshot_path FROM pastes WHERE forked_from IS NOT NULL');
- // Unlink the fork screenshots so they don't leak on disk.
- for (const f of forks) {
- if (f.screenshot_path) { try { fs.unlinkSync(f.screenshot_path); } catch { /* ignore */ } }
- }
- run('DELETE FROM pastes WHERE forked_from IS NOT NULL');
- return forks.length;
-}
-
function getPasteStats() {
function getPasteStats() {
const total = get('SELECT COUNT(*) as c FROM pastes')?.c || 0;
const total = get('SELECT COUNT(*) as c FROM pastes')?.c || 0;
const textPastes = get("SELECT COUNT(*) as c FROM pastes WHERE type = 'paste'")?.c || 0;
const textPastes = get("SELECT COUNT(*) as c FROM pastes WHERE type = 'paste'")?.c || 0;
@@ -8437,7 +8342,7 @@ module.exports = {
@@ -8437,7 +8342,7 @@ module.exports = {
setVodAiOverview, setClipAiOverview, setVodTranscript, setClipTranscript, getStreamMemoriesInRange,
setVodAiOverview, setClipAiOverview, setVodTranscript, setClipTranscript, getStreamMemoriesInRange,
getVodsNeedingOverview, getClipsNeedingOverview, getVodsNeedingTimeline, getVodsNeedingTranscript, getClipsNeedingTranscript, getPastesNeedingAnalysis,
getVodsNeedingOverview, getClipsNeedingOverview, getVodsNeedingTimeline, getVodsNeedingTranscript, getClipsNeedingTranscript, getPastesNeedingAnalysis,
setVodTranscriptStatus, setClipTranscriptStatus, bumpVodTranscriptAttempt, bumpClipTranscriptAttempt,
setVodTranscriptStatus, setClipTranscriptStatus, bumpVodTranscriptAttempt, bumpClipTranscriptAttempt,
- updatePasteAi, cleanupMalformedAiText, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,
+ cleanupMalformedAiText, recordAiUsage, getAiCostToday, getAiCostTodayForUser, getAiUsageSummary,
getStreamMemoriesByUser, countStreamMemoriesByUser, getAiMomentCandidates, getStreamTranscriptSegments, getUserPastesForAi,
getStreamMemoriesByUser, countStreamMemoriesByUser, getAiMomentCandidates, getStreamTranscriptSegments, getUserPastesForAi,
addTimelineEvents, getTimeline, getTimelineText, getTimelineCoverage, linkTimelineToVod, getTimelineByVod, getTimelineVodId,
addTimelineEvents, getTimeline, getTimelineText, getTimelineCoverage, linkTimelineToVod, getTimelineByVod, getTimelineVodId,
getVodsForMomentRanking, getClipStartTimesForStream, getChatSpikeOffsets,
getVodsForMomentRanking, getClipStartTimesForStream, getChatSpikeOffsets,
@@ -8508,7 +8413,7 @@ module.exports = {
@@ -8508,7 +8413,7 @@ module.exports = {
renormalizePendingMediaRequestPositions,
renormalizePendingMediaRequestPositions,
// VODs
// VODs
getVodById, getVodsByUser, countVodsByUser, getPublicVods, countPublicVods, listVodStreamers, getActiveVodByStream,
getVodById, getVodsByUser, countVodsByUser, getPublicVods, countPublicVods, listVodStreamers, getActiveVodByStream,
- updateVodHealth, repairVodDuration, getVodHealthById, getVodScanCandidates,
+ getVodHealthById, getVodScanCandidates,
getVodsNeedingHealthScan, getQuarantinedVodsForCleanup,
getVodsNeedingHealthScan, getQuarantinedVodsForCleanup,
// Clips
// Clips
getClipById, getClipsByUser, countClipsByUser, getPublicClips, countPublicClips, listClipStreamers, getClipsByStream, getClipsOfUserStreams,
getClipById, getClipsByUser, countClipsByUser, getPublicClips, countPublicClips, listClipStreamers, getClipsByStream, getClipsOfUserStreams,
@@ -8558,9 +8463,8 @@ module.exports = {
@@ -8558,9 +8463,8 @@ module.exports = {
getChannelModerationSettings, upsertChannelModerationSettings,
getChannelModerationSettings, upsertChannelModerationSettings,
// Pastes
// Pastes
getPasteBySlug, getPasteById, listPastes,
getPasteBySlug, getPasteById, listPastes,
- incrementPasteViews, updatePaste, deletePaste, getUserPastes, getUserPastesForChannel, countUserPastesForChannel,
- likePaste, unlikePaste, hasUserLikedPaste, incrementPasteCopies,
- countUserPastesToday, getLastPasteTime, deleteAllForks, getPasteStats, getUserTotalGameLevel,
+ getUserPastes, getUserPastesForChannel, countUserPastesForChannel,
+ hasUserLikedPaste, countUserPastesToday, getLastPasteTime, getPasteStats, getUserTotalGameLevel,
// Paste Comments
// Paste Comments
createPasteComment, getPasteComments, getPasteCommentReplies,
createPasteComment, getPasteComments, getPasteCommentReplies,
getPasteCommentById, getPasteCommentCount, deletePasteComment,
getPasteCommentById, getPasteCommentCount, deletePasteComment,
new file mode 100644
new file mode 100644
@@ -0,0 +1,37 @@
@@ -0,0 +1,37 @@
+'use strict';
+
+// Live's legacy vods/clips/pastes tables are frozen: Media and Community own that data now
+// (AGENTS.md, Wave 22 register C-73). Live may read them for pre-cutover pages, but no server code
+// may write them. The only writes allowed are the two guarded one-time visibility backfills that
+// run when the column is first added.
+
+const assert = require('assert');
+const fs = require('fs');
+const path = require('path');
+
+const ROOT = path.join(__dirname, '..');
+const WRITE = /\b(INSERT(?:\s+OR\s+\w+)?\s+INTO|UPDATE|DELETE\s+FROM|REPLACE\s+INTO)\s+(vods|clips|pastes|paste_likes)\b/i;
+const ALLOWED = [
+ "UPDATE vods SET visibility = CASE WHEN is_public = 1 THEN 'public' ELSE 'private' END",
+ "UPDATE clips SET visibility = CASE WHEN is_public = 1 THEN 'public' ELSE 'unlisted' END",
+];
+
+function walk(dir, out = []) {
+ for (const e of fs.readdirSync(dir, { withFileTypes: true })) {
+ const p = path.join(dir, e.name);
+ if (e.isDirectory()) walk(p, out);
+ else if (e.name.endsWith('.js')) out.push(p);
+ }
+ return out;
+}
+
+const offenders = [];
+for (const file of [...walk(path.join(ROOT, 'server')), ...walk(path.join(ROOT, 'scripts'))]) {
+ fs.readFileSync(file, 'utf8').split('\n').forEach((line, i) => {
+ if (!WRITE.test(line) || /^\s*(\/\/|\*)/.test(line)) return;
+ if (ALLOWED.some((a) => line.includes(a))) return;
+ offenders.push(`${path.relative(ROOT, file)}:${i + 1}: ${line.trim().slice(0, 120)}`);
+ });
+}
+assert.deepStrictEqual(offenders, [], `writes to frozen tables:\n${offenders.join('\n')}`);
+console.log('frozen tables: all checks passed');