2026-08-14 14:17:54 +02:00
import { randomUUID } from 'node:crypto'
import type { PoolClient } from 'pg'
type IdMap = Map < string , string >
function mapped ( ids : IdMap , sourceId : string , label : string ) {
const id = ids . get ( sourceId )
if ( ! id ) throw new Error ( `Could not map ${ label } ${ sourceId } ` )
return id
}
export async function clearBoard ( client : PoolClient , boardId : string ) {
2026-08-17 09:24:53 +02:00
await client . query ( 'DELETE FROM osint.board_views WHERE board_id=$1' , [ boardId ])
2026-08-14 14:44:58 +02:00
await client . query ( 'DELETE FROM osint.brief_concepts WHERE board_id=$1' , [ boardId ])
await client . query ( 'DELETE FROM osint.level_briefs WHERE board_id=$1' , [ boardId ])
2026-08-14 14:17:54 +02:00
await client . query ( 'DELETE FROM osint.metadata_fields WHERE board_id=$1' , [ boardId ])
await client . query ( 'DELETE FROM osint.exhibits WHERE board_id=$1' , [ boardId ])
await client . query ( 'UPDATE osint.boards SET revision=0,updated_at=NOW() WHERE id=$1' , [ boardId ])
}
/** Clone a complete normalized board. The target board must be empty. */
export async function cloneBoard ( client : PoolClient , sourceBoardId : string , targetBoardId : string ) {
const exhibitIds : IdMap = new Map ()
const regionIds : IdMap = new Map ()
const fieldIds : IdMap = new Map ()
2026-08-17 09:24:53 +02:00
const views = await client . query < {
id : string ; view_type_id : string ; placement_mode : string ; dock_edge : string | null ; xpos : number | null ; ypos : number | null
width : number | null ; height : number ; z_index : number ; visible : boolean ; range_mode : string | null ; range_start : string | null ; range_end : string | null
} > ( `SELECT v.id,v.view_type_id,v.placement_mode,v.dock_edge,v.xpos,v.ypos,v.width,v.height,v.z_index,v.visible,
t.range_mode,t.range_start::text,t.range_end::text FROM osint.board_views v
LEFT JOIN osint.timeline_views t ON t.view_id=v.id WHERE v.board_id=$1 ORDER BY v.z_index,v.created_at` , [ sourceBoardId ])
for ( const row of views . rows ) {
const viewId = randomUUID ()
await client . query ( `INSERT INTO osint.board_views
(id,board_id,view_type_id,origin_view_id,placement_mode,dock_edge,xpos,ypos,width,height,z_index,visible)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11,$12)` , [ viewId , targetBoardId , row . view_type_id , row . id , row . placement_mode , row . dock_edge ,
row . xpos , row . ypos , row . width , row . height , row . z_index , row . visible ])
if ( row . view_type_id === 'timeline' ) await client . query (
'INSERT INTO osint.timeline_views (view_id,range_mode,range_start,range_end) VALUES ($1,$2,$3,$4)' ,
[ viewId , row . range_mode || 'auto' , row . range_start , row . range_end ])
}
2026-08-14 15:40:54 +02:00
2026-08-14 14:17:54 +02:00
const exhibits = await client . query < {
id : string ; exhibit_type_id : string ; xpos : number ; ypos : number ; width : number ; height : number
rotation : number ; z_index : number ; hidden : boolean
} > ( `SELECT id,exhibit_type_id,xpos,ypos,width,height,rotation,z_index,hidden
FROM osint.exhibits WHERE board_id=$1 ORDER BY created_at,id` , [ sourceBoardId ])
for ( const row of exhibits . rows ) {
const id = randomUUID (); exhibitIds . set ( row . id , id )
await client . query ( `INSERT INTO osint.exhibits
(id,board_id,exhibit_type_id,origin_exhibit_id,xpos,ypos,width,height,rotation,z_index,hidden)
VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10,$11)` ,
[ id , targetBoardId , row . exhibit_type_id , row . id , row . xpos , row . ypos , row . width , row . height , row . rotation , row . z_index , row . hidden ])
}
const folders = await client . query < { exhibit_id : string ; title : string ; label_text : string ; is_open : boolean } > (
`SELECT f.* FROM osint.folder_exhibits f JOIN osint.exhibits e ON e.id=f.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of folders . rows ) await client . query (
'INSERT INTO osint.folder_exhibits (exhibit_id,title,label_text,is_open) VALUES ($1,$2,$3,$4)' ,
[ mapped ( exhibitIds , row . exhibit_id , 'folder' ), row . title , row . label_text , row . is_open ])
const documents = await client . query < {
exhibit_id : string ; document_type_id : string ; asset_id : string | null ; title : string
published_at : Date | null ; captured_at : Date | null ; source_uri : string | null
} > ( `SELECT d.* FROM osint.document_exhibits d JOIN osint.exhibits e ON e.id=d.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of documents . rows ) await client . query ( `INSERT INTO osint.document_exhibits
(exhibit_id,document_type_id,asset_id,title,published_at,captured_at,source_uri) VALUES ($1,$2,$3,$4,$5,$6,$7)` ,
[ mapped ( exhibitIds , row . exhibit_id , 'document' ), row . document_type_id , row . asset_id , row . title , row . published_at , row . captured_at , row . source_uri ])
const images = await client . query < { exhibit_id : string ; pixel_width : number | null ; pixel_height : number | null ; alt_text : string } > (
`SELECT i.* FROM osint.image_documents i JOIN osint.exhibits e ON e.id=i.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of images . rows ) await client . query (
'INSERT INTO osint.image_documents (exhibit_id,pixel_width,pixel_height,alt_text) VALUES ($1,$2,$3,$4)' ,
[ mapped ( exhibitIds , row . exhibit_id , 'image' ), row . pixel_width , row . pixel_height , row . alt_text ])
const notes = await client . query < { exhibit_id : string ; title : string ; note_text : string } > (
`SELECT n.* FROM osint.note_exhibits n JOIN osint.exhibits e ON e.id=n.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of notes . rows ) await client . query ( 'INSERT INTO osint.note_exhibits (exhibit_id,title,note_text) VALUES ($1,$2,$3)' ,
[ mapped ( exhibitIds , row . exhibit_id , 'note' ), row . title , row . note_text ])
2026-08-14 19:57:22 +02:00
const events = await client . query < { exhibit_id : string ; title : string ; narrative_text : string ; occurred_at : Date | null } > (
2026-08-14 14:17:54 +02:00
`SELECT ev.* FROM osint.event_exhibits ev JOIN osint.exhibits e ON e.id=ev.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of events . rows ) await client . query (
'INSERT INTO osint.event_exhibits (exhibit_id,title,narrative_text,occurred_at) VALUES ($1,$2,$3,$4)' ,
[ mapped ( exhibitIds , row . exhibit_id , 'event' ), row . title , row . narrative_text , row . occurred_at ])
2026-08-14 14:44:58 +02:00
const parties = await client . query < { exhibit_id : string ; party_kind : string ; display_name : string ; summary : string } > (
`SELECT p.* FROM osint.party_exhibits p JOIN osint.exhibits e ON e.id=p.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of parties . rows ) await client . query (
'INSERT INTO osint.party_exhibits (exhibit_id,party_kind,display_name,summary) VALUES ($1,$2,$3,$4)' ,
[ mapped ( exhibitIds , row . exhibit_id , 'party' ), row . party_kind , row . display_name , row . summary ])
const people = await client . query < { exhibit_id : string ; given_name : string | null ; family_name : string | null } > (
`SELECT p.* FROM osint.person_parties p JOIN osint.exhibits e ON e.id=p.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of people . rows ) await client . query ( 'INSERT INTO osint.person_parties (exhibit_id,given_name,family_name) VALUES ($1,$2,$3)' ,
[ mapped ( exhibitIds , row . exhibit_id , 'person' ), row . given_name , row . family_name ])
const organizations = await client . query < { exhibit_id : string ; organization_kind : string } > (
`SELECT o.* FROM osint.organization_parties o JOIN osint.exhibits e ON e.id=o.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of organizations . rows ) await client . query ( 'INSERT INTO osint.organization_parties (exhibit_id,organization_kind) VALUES ($1,$2)' ,
[ mapped ( exhibitIds , row . exhibit_id , 'organization' ), row . organization_kind ])
const aliases = await client . query < { party_exhibit_id : string ; alias : string ; sort_order : number } > (
`SELECT a.party_exhibit_id,a.alias,a.sort_order FROM osint.party_aliases a JOIN osint.exhibits e ON e.id=a.party_exhibit_id
WHERE e.board_id=$1 ORDER BY a.sort_order` , [ sourceBoardId ])
for ( const row of aliases . rows ) await client . query ( 'INSERT INTO osint.party_aliases (id,party_exhibit_id,alias,sort_order) VALUES ($1,$2,$3,$4)' ,
[ randomUUID (), mapped ( exhibitIds , row . party_exhibit_id , 'party alias' ), row . alias , row . sort_order ])
2026-08-14 14:17:54 +02:00
const blocks = await client . query < { document_exhibit_id : string ; sort_order : number ; content : string } > (
`SELECT b.document_exhibit_id,b.sort_order,b.content FROM osint.document_content_blocks b
JOIN osint.exhibits e ON e.id=b.document_exhibit_id WHERE e.board_id=$1 ORDER BY b.sort_order` , [ sourceBoardId ])
for ( const row of blocks . rows ) await client . query (
'INSERT INTO osint.document_content_blocks (id,document_exhibit_id,sort_order,content) VALUES ($1,$2,$3,$4)' ,
[ randomUUID (), mapped ( exhibitIds , row . document_exhibit_id , 'content document' ), row . sort_order , row . content ])
const regions = await client . query < {
id : string ; document_exhibit_id : string ; region_key : string ; label : string ; excerpt : string ; occurred_at : Date | null ; sort_order : number
} > ( `SELECT r.* FROM osint.document_regions r JOIN osint.exhibits e ON e.id=r.document_exhibit_id
WHERE e.board_id=$1 ORDER BY r.sort_order` , [ sourceBoardId ])
for ( const row of regions . rows ) {
const id = randomUUID (); regionIds . set ( row . id , id )
await client . query ( `INSERT INTO osint.document_regions
(id,document_exhibit_id,region_key,label,excerpt,occurred_at,sort_order) VALUES ($1,$2,$3,$4,$5,$6,$7)` ,
[ id , mapped ( exhibitIds , row . document_exhibit_id , 'region document' ), row . region_key , row . label , row . excerpt , row . occurred_at , row . sort_order ])
}
const fields = await client . query < { id : string ; field_key : string ; label : string ; value_type : string } > (
'SELECT id,field_key,label,value_type FROM osint.metadata_fields WHERE board_id=$1 ORDER BY field_key' , [ sourceBoardId ])
for ( const row of fields . rows ) {
const id = randomUUID (); fieldIds . set ( row . id , id )
await client . query ( 'INSERT INTO osint.metadata_fields (id,board_id,field_key,label,value_type) VALUES ($1,$2,$3,$4,$5)' ,
[ id , targetBoardId , row . field_key , row . label , row . value_type ])
}
for ( const [ table , cast ] of [
[ 'exhibit_metadata_text_values' , 'text' ], [ 'exhibit_metadata_timestamp_values' , 'timestamptz' ],
[ 'exhibit_metadata_number_values' , 'numeric' ], [ 'exhibit_metadata_boolean_values' , 'boolean' ],
] as const ) {
const values = await client . query < { exhibit_id : string ; field_id : string ; value : unknown } > (
`SELECT v.exhibit_id,v.field_id,v.value:: ${ cast } AS value FROM osint. ${ table } v
JOIN osint.exhibits e ON e.id=v.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of values . rows ) await client . query ( `INSERT INTO osint. ${ table } (exhibit_id,field_id,value) VALUES ($1,$2,$3)` ,
[ mapped ( exhibitIds , row . exhibit_id , 'metadata exhibit' ), mapped ( fieldIds , row . field_id , 'metadata field' ), row . value ])
}
const memberships = await client . query < { folder_exhibit_id : string ; child_exhibit_id : string ; sort_order : number } > (
'SELECT folder_exhibit_id,child_exhibit_id,sort_order FROM osint.folder_memberships WHERE board_id=$1' , [ sourceBoardId ])
for ( const row of memberships . rows ) await client . query ( `INSERT INTO osint.folder_memberships
(board_id,folder_exhibit_id,child_exhibit_id,sort_order) VALUES ($1,$2,$3,$4)` ,
[ targetBoardId , mapped ( exhibitIds , row . folder_exhibit_id , 'membership folder' ), mapped ( exhibitIds , row . child_exhibit_id , 'membership child' ), row . sort_order ])
const eventEvidence = await client . query < { event_exhibit_id : string ; evidence_exhibit_id : string ; sort_order : number ; note : string | null } > (
'SELECT event_exhibit_id,evidence_exhibit_id,sort_order,note FROM osint.event_evidence WHERE board_id=$1' , [ sourceBoardId ])
for ( const row of eventEvidence . rows ) await client . query ( `INSERT INTO osint.event_evidence
(board_id,event_exhibit_id,evidence_exhibit_id,sort_order,note) VALUES ($1,$2,$3,$4,$5)` ,
[ targetBoardId , mapped ( exhibitIds , row . event_exhibit_id , 'event' ), mapped ( exhibitIds , row . evidence_exhibit_id , 'event evidence' ), row . sort_order , row . note ])
2026-08-14 14:44:58 +02:00
const partyEvidence = await client . query < { party_exhibit_id : string ; evidence_exhibit_id : string ; sort_order : number ; note : string | null } > (
'SELECT party_exhibit_id,evidence_exhibit_id,sort_order,note FROM osint.party_evidence WHERE board_id=$1' , [ sourceBoardId ])
for ( const row of partyEvidence . rows ) await client . query ( `INSERT INTO osint.party_evidence
(board_id,party_exhibit_id,evidence_exhibit_id,sort_order,note) VALUES ($1,$2,$3,$4,$5)` ,
[ targetBoardId , mapped ( exhibitIds , row . party_exhibit_id , 'party' ), mapped ( exhibitIds , row . evidence_exhibit_id , 'party evidence' ), row . sort_order , row . note ])
const partyRelationships = await client . query < { relationship_type_id : string ; from_party_exhibit_id : string ; to_party_exhibit_id : string ; note : string | null } > (
'SELECT relationship_type_id,from_party_exhibit_id,to_party_exhibit_id,note FROM osint.party_relationships WHERE board_id=$1' , [ sourceBoardId ])
for ( const row of partyRelationships . rows ) await client . query ( `INSERT INTO osint.party_relationships
(id,board_id,relationship_type_id,from_party_exhibit_id,to_party_exhibit_id,note) VALUES ($1,$2,$3,$4,$5,$6)` ,
[ randomUUID (), targetBoardId , row . relationship_type_id , mapped ( exhibitIds , row . from_party_exhibit_id , 'related party' ),
mapped ( exhibitIds , row . to_party_exhibit_id , 'related party' ), row . note ])
2026-08-14 18:26:20 +02:00
const connections = await client . query < { connection_type_id : string ; from_exhibit_id : string ; to_exhibit_id : string ; label : string | null ; tightness : number ; tag_style : string ; tag_position_percent : number ; tag_lateral_offset : number } > (
'SELECT connection_type_id,from_exhibit_id,to_exhibit_id,label,tightness,tag_style,tag_position_percent,tag_lateral_offset FROM osint.exhibit_connections WHERE board_id=$1' , [ sourceBoardId ])
2026-08-14 14:17:54 +02:00
for ( const row of connections . rows ) await client . query ( `INSERT INTO osint.exhibit_connections
2026-08-14 18:26:20 +02:00
(id,board_id,connection_type_id,from_exhibit_id,to_exhibit_id,label,tightness,tag_style,tag_position_percent,tag_lateral_offset) VALUES ($1,$2,$3,$4,$5,$6,$7,$8,$9,$10)` ,
[ randomUUID (), targetBoardId , row . connection_type_id , mapped ( exhibitIds , row . from_exhibit_id , 'connection source' ), mapped ( exhibitIds , row . to_exhibit_id , 'connection target' ), row . label , row . tightness , row . tag_style , row . tag_position_percent , row . tag_lateral_offset ])
2026-08-14 14:17:54 +02:00
const sources = await client . query < { exhibit_id : string ; source_document_exhibit_id : string ; source_region_id : string | null } > (
`SELECT s.exhibit_id,s.source_document_exhibit_id,s.source_region_id FROM osint.exhibit_sources s
JOIN osint.exhibits e ON e.id=s.exhibit_id WHERE e.board_id=$1` , [ sourceBoardId ])
for ( const row of sources . rows ) await client . query ( `INSERT INTO osint.exhibit_sources
(exhibit_id,source_document_exhibit_id,source_region_id) VALUES ($1,$2,$3)` ,
[ mapped ( exhibitIds , row . exhibit_id , 'sourced exhibit' ), mapped ( exhibitIds , row . source_document_exhibit_id , 'source document' ),
row . source_region_id ? mapped ( regionIds , row . source_region_id , 'source region' ) : null ])
2026-08-14 14:44:58 +02:00
const brief = await client . query < { body : string } > ( 'SELECT body FROM osint.level_briefs WHERE board_id=$1' , [ sourceBoardId ])
if ( brief . rows [ 0 ]) await client . query ( 'INSERT INTO osint.level_briefs (board_id,body) VALUES ($1,$2)' , [ targetBoardId , brief . rows [ 0 ]. body ])
const concepts = await client . query < {
id : string ; label : string ; context_text : string ; sort_order : number ; expected_party_kind : string | null ; resolved_party_exhibit_id : string | null
} > ( 'SELECT id,label,context_text,sort_order,expected_party_kind,resolved_party_exhibit_id FROM osint.brief_concepts WHERE board_id=$1 ORDER BY sort_order,id' , [ sourceBoardId ])
for ( const row of concepts . rows ) await client . query ( `INSERT INTO osint.brief_concepts
(id,board_id,origin_concept_id,label,context_text,sort_order,expected_party_kind,resolved_party_exhibit_id) VALUES ($1,$2,$3,$4,$5,$6,$7,$8)` ,
[ randomUUID (), targetBoardId , row . id , row . label , row . context_text , row . sort_order , row . expected_party_kind ,
row . resolved_party_exhibit_id ? mapped ( exhibitIds , row . resolved_party_exhibit_id , 'resolved party' ) : null ])
2026-08-14 14:17:54 +02:00
return exhibitIds
}