coulomb-social/scripts/bubble-resource-utils.mjs
tegwick 37e3bfc2c7
Some checks failed
Use-case regression gate / usecase-regressions (push) Failing after 7m29s
Recover original attachments through the confirmed Bubble modal
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a192e9-c28b-7533-bf25-e2341364c22a
2026-10-31 20:37:49 +01:00

78 lines
3.9 KiB
JavaScript

/** Select typed content resources; never retain user/session envelopes or raw responses. */
import {normalizeBubbleRef} from './bubble-corpus-utils.mjs';
export const RESOURCE_TYPES = new Set(['custom.scrap', 'custom.relattachment', 'custom.attachment']);
const validId = value => typeof value === 'string' && /^[0-9]+x[0-9]+$/.test(value);
/** Bind observed attachment relations to child articles and their original files. */
export function attachmentRows(parent, resources, articles) {
const rows=[];
for (const [key,versions] of Object.entries(resources.records)) {
if (!key.startsWith('custom.relattachment:')) continue;
if (!versions.some(r=>normalizeBubbleRef(r.to_custom_article)===parent)) continue;
if (versions.length!==1) throw new Error('attachment_relation_drift');
const relation=versions[0], id=normalizeBubbleRef(relation._chunk_custom_article);
const child=articles[id];
if (!validId(id)||!child||normalizeBubbleRef(child.context_custom_article)!==parent)
throw new Error('attachment_child_binding');
const source=child.logo_image;
if (typeof source!=='string') throw new Error('attachment_file_missing');
const url=new URL(source.startsWith('//')?'https:'+source:source);
if (url.protocol!=='https:'||url.username||url.password||url.port||url.hash||
!/^[a-f0-9]{32}\.cdn\.bubble\.io$/.test(url.hostname))
throw new Error('attachment_file_origin');
rows.push({relation_id:normalizeBubbleRef(relation._id),article_id:id,
url:url.href,name:child.nomer_text||'',created_at:child['Created Date'],
modified_at:child['Modified Date']});
}
if (rows.length>100) throw new Error('attachment_count_bound');
return rows;
}
export function resourceParents(articles) {
return Object.entries(articles).filter(([,a]) => a.hasattachments_boolean === true ||
(Object.hasOwn(a, '_list_custom_scrap') &&
!(Array.isArray(a._list_custom_scrap) && a._list_custom_scrap.length === 0)))
.map(([id]) => normalizeBubbleRef(id)).filter(validId).sort();
}
export function ingestResourceBody(text, output) {
const visit = obj => {
if (Array.isArray(obj)) { obj.forEach(visit); return; }
if (!obj || typeof obj !== 'object') return;
// Typed initialization is a terminal envelope. Never walk into unrelated
// user/session data, even if it contains something resembling a resource.
if (typeof obj.type === 'string' && Object.hasOwn(obj, 'data')) {
const id = normalizeBubbleRef(obj.id), data = obj.data;
if (!RESOURCE_TYPES.has(obj.type) || !validId(id)) return;
if (data && typeof data === 'object' && !Array.isArray(data) &&
Object.keys(data).length === 0) {
output.empty[`${obj.type}:${id}`] = {type:obj.type,id};
} else if (data && data._type === obj.type && normalizeBubbleRef(data._id) === id) {
visit({_type:obj.type,_id:id,_source:data});
}
return;
}
if (RESOURCE_TYPES.has(obj._type)) {
const id = normalizeBubbleRef(obj._id), data = obj._source;
if (validId(id) && data && typeof data === 'object' && !Array.isArray(data) &&
(!data._id || normalizeBubbleRef(data._id) === id) &&
(!data._type || data._type === obj._type)) {
const key = `${obj._type}:${id}`;
// Keep source observations, not a guessed union of different versions.
const observed = {_type:obj._type,_id:id,...data};
const versions = output.records[key] ||= [];
if (!versions.some(v => JSON.stringify(v) === JSON.stringify(observed))) versions.push(observed);
}
return;
}
if (Array.isArray(obj.responses)) obj.responses.forEach(visit);
if (obj.hits && Array.isArray(obj.hits.hits)) obj.hits.hits.forEach(visit);
};
try { visit(JSON.parse(text)); }
catch {
for (const line of text.split('\n')) {
try { visit(JSON.parse(line)); } catch { /* Not a JSON record. */ }
}
}
}