2025-09-11 16:23:32 -07:00
/ * *
* Data Management API for Brainy 3.0
* Provides backup , restore , import , export , and data management
* /
import { StorageAdapter , HNSWNoun , GraphVerb } from '../coreTypes.js'
import { Entity , Relation } from '../types/brainy.types.js'
import { NounType , VerbType } from '../types/graphTypes.js'
export interface BackupOptions {
includeVectors? : boolean
compress? : boolean
format ? : 'json' | 'binary'
}
export interface RestoreOptions {
merge? : boolean
overwrite? : boolean
validate? : boolean
}
export interface ImportOptions {
format : 'json' | 'csv'
mapping? : Record < string , string >
batchSize? : number
validate? : boolean
}
export interface ExportOptions {
format ? : 'json' | 'csv'
filter ? : {
type ? : NounType | NounType [ ]
where? : Record < string , any >
service? : string
}
includeVectors? : boolean
}
export interface BackupData {
version : string
timestamp : number
entities : Array < {
id : string
vector? : number [ ]
type : string
metadata : any
service? : string
} >
relations : Array < {
id : string
from : string
to : string
type : string
weight : number
metadata? : any
} >
config? : Record < string , any >
stats : {
entityCount : number
relationCount : number
vectorDimensions? : number
}
}
export interface ImportResult {
successful : number
failed : number
errors : Array < { item : any ; error : string } >
duration : number
}
export class DataAPI {
private brain : any // Reference to Brainy instance for neural import
constructor (
private storage : StorageAdapter ,
private getEntity : ( id : string ) = > Promise < Entity | null > ,
private getRelation ? : ( id : string ) = > Promise < Relation | null > ,
brain? : any
) {
this . brain = brain
}
/ * *
* Create a backup of all data
* /
async backup ( options : BackupOptions = { } ) : Promise < BackupData | { compressed : boolean ; data : string ; originalSize : number ; compressedSize : number } > {
const {
includeVectors = true ,
compress = false ,
format = 'json'
} = options
const startTime = Date . now ( )
// Get all entities
const nounsResult = await this . storage . getNouns ( {
pagination : { limit : 1000000 }
} )
const entities : BackupData [ 'entities' ] = [ ]
for ( const noun of nounsResult . items ) {
const entity = {
id : noun.id ,
vector : includeVectors ? noun.vector : undefined ,
fix(storage): v4.8.0 metadata architecture refactoring - FIXES VFS bug
CRITICAL FIX: VFS bug that persisted through v4.5.1-v4.7.4 is NOW FIXED.
Root Cause:
- Storage adapters were not properly extracting standard fields from metadata
- This caused getVerbsBySource_internal() to return 0 relationships despite relationships existing
- VFS PathResolver couldn't navigate directory structure
Solution - Metadata Architecture Refactoring:
1. Move standard fields to top-level of HNSWNounWithMetadata and HNSWVerbWithMetadata
- type, createdAt, updatedAt, confidence, weight, service, data, createdBy
2. Update all 9 storage adapters to extract standard fields from metadata on load
3. Maintain backward compatibility at storage layer (metadata files unchanged)
Changes:
- src/coreTypes.ts: Update HNSWNounWithMetadata and HNSWVerbWithMetadata interfaces
- Add top-level standard fields
- Change data type from unknown to Record<string, any>
- Add confidence field to GraphVerb
- src/storage/baseStorage.ts: Add type cast pattern for standard field extraction
- src/storage/adapters/*.ts: Fix all 9 adapters (memoryStorage, fileSystemStorage, gcsStorage,
s3CompatibleStorage, r2Storage, opfsStorage, azureBlobStorage, typeAwareStorageAdapter)
- Extract standard fields from metadata on load
- Place at top-level of returned entities
- src/api/DataAPI.ts: Read fields from top-level instead of metadata
- src/graph/graphAdjacencyIndex.ts: Convert HNSWVerbWithMetadata to GraphVerb format
- src/utils/metadataIndex.ts: Fix typo (metadata → entityOrMetadata)
- src/types/brainy.types.ts: Add createdBy field to AddParams
- src/types/graphTypes.ts: Add service field to GraphVerb
Test Results:
✅ VFS bug FIXED - vfs.readdir('/') now returns files (was returning empty array)
✅ getVerbsBySource_internal() now returns relationships correctly
✅ Build succeeds with ZERO compilation errors
✅ 95.7% of tests pass (954/997)
Breaking Changes:
- None - backward compatibility maintained at storage layer
Version: 4.8.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 15:43:49 -07:00
type : noun . type || NounType . Thing , // v4.8.0: type at top-level
2025-09-11 16:23:32 -07:00
metadata : noun.metadata ,
fix(storage): v4.8.0 metadata architecture refactoring - FIXES VFS bug
CRITICAL FIX: VFS bug that persisted through v4.5.1-v4.7.4 is NOW FIXED.
Root Cause:
- Storage adapters were not properly extracting standard fields from metadata
- This caused getVerbsBySource_internal() to return 0 relationships despite relationships existing
- VFS PathResolver couldn't navigate directory structure
Solution - Metadata Architecture Refactoring:
1. Move standard fields to top-level of HNSWNounWithMetadata and HNSWVerbWithMetadata
- type, createdAt, updatedAt, confidence, weight, service, data, createdBy
2. Update all 9 storage adapters to extract standard fields from metadata on load
3. Maintain backward compatibility at storage layer (metadata files unchanged)
Changes:
- src/coreTypes.ts: Update HNSWNounWithMetadata and HNSWVerbWithMetadata interfaces
- Add top-level standard fields
- Change data type from unknown to Record<string, any>
- Add confidence field to GraphVerb
- src/storage/baseStorage.ts: Add type cast pattern for standard field extraction
- src/storage/adapters/*.ts: Fix all 9 adapters (memoryStorage, fileSystemStorage, gcsStorage,
s3CompatibleStorage, r2Storage, opfsStorage, azureBlobStorage, typeAwareStorageAdapter)
- Extract standard fields from metadata on load
- Place at top-level of returned entities
- src/api/DataAPI.ts: Read fields from top-level instead of metadata
- src/graph/graphAdjacencyIndex.ts: Convert HNSWVerbWithMetadata to GraphVerb format
- src/utils/metadataIndex.ts: Fix typo (metadata → entityOrMetadata)
- src/types/brainy.types.ts: Add createdBy field to AddParams
- src/types/graphTypes.ts: Add service field to GraphVerb
Test Results:
✅ VFS bug FIXED - vfs.readdir('/') now returns files (was returning empty array)
✅ getVerbsBySource_internal() now returns relationships correctly
✅ Build succeeds with ZERO compilation errors
✅ 95.7% of tests pass (954/997)
Breaking Changes:
- None - backward compatibility maintained at storage layer
Version: 4.8.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 15:43:49 -07:00
service : noun.service // v4.8.0: service at top-level
2025-09-11 16:23:32 -07:00
}
entities . push ( entity )
}
// Get all relations
const verbsResult = await this . storage . getVerbs ( {
pagination : { limit : 1000000 }
} )
const relations : BackupData [ 'relations' ] = [ ]
for ( const verb of verbsResult . items ) {
relations . push ( {
id : verb.id ,
from : verb . sourceId ,
to : verb.targetId ,
2025-10-17 12:29:27 -07:00
type : verb . verb as string ,
fix(storage): v4.8.0 metadata architecture refactoring - FIXES VFS bug
CRITICAL FIX: VFS bug that persisted through v4.5.1-v4.7.4 is NOW FIXED.
Root Cause:
- Storage adapters were not properly extracting standard fields from metadata
- This caused getVerbsBySource_internal() to return 0 relationships despite relationships existing
- VFS PathResolver couldn't navigate directory structure
Solution - Metadata Architecture Refactoring:
1. Move standard fields to top-level of HNSWNounWithMetadata and HNSWVerbWithMetadata
- type, createdAt, updatedAt, confidence, weight, service, data, createdBy
2. Update all 9 storage adapters to extract standard fields from metadata on load
3. Maintain backward compatibility at storage layer (metadata files unchanged)
Changes:
- src/coreTypes.ts: Update HNSWNounWithMetadata and HNSWVerbWithMetadata interfaces
- Add top-level standard fields
- Change data type from unknown to Record<string, any>
- Add confidence field to GraphVerb
- src/storage/baseStorage.ts: Add type cast pattern for standard field extraction
- src/storage/adapters/*.ts: Fix all 9 adapters (memoryStorage, fileSystemStorage, gcsStorage,
s3CompatibleStorage, r2Storage, opfsStorage, azureBlobStorage, typeAwareStorageAdapter)
- Extract standard fields from metadata on load
- Place at top-level of returned entities
- src/api/DataAPI.ts: Read fields from top-level instead of metadata
- src/graph/graphAdjacencyIndex.ts: Convert HNSWVerbWithMetadata to GraphVerb format
- src/utils/metadataIndex.ts: Fix typo (metadata → entityOrMetadata)
- src/types/brainy.types.ts: Add createdBy field to AddParams
- src/types/graphTypes.ts: Add service field to GraphVerb
Test Results:
✅ VFS bug FIXED - vfs.readdir('/') now returns files (was returning empty array)
✅ getVerbsBySource_internal() now returns relationships correctly
✅ Build succeeds with ZERO compilation errors
✅ 95.7% of tests pass (954/997)
Breaking Changes:
- None - backward compatibility maintained at storage layer
Version: 4.8.0
🤖 Generated with [Claude Code](https://claude.com/claude-code)
Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-27 15:43:49 -07:00
weight : verb.weight || 1.0 , // v4.8.0: weight at top-level
2025-09-11 16:23:32 -07:00
metadata : verb.metadata
} )
}
// Create backup data
const backupData : BackupData = {
version : '3.0.0' ,
timestamp : Date.now ( ) ,
entities ,
relations ,
stats : {
entityCount : entities.length ,
relationCount : relations.length ,
vectorDimensions : entities [ 0 ] ? . vector ? . length
}
}
// Compress if requested
if ( compress ) {
// Import zlib for compression
feat: add node: protocol to all Node.js built-in imports for bundler compatibility
- Updated all fs, path, crypto, os, url, util, events, http, https, net, child_process, stream, and zlib imports
- Changed both static imports and dynamic imports to use node: protocol
- This makes Brainy more bundler-friendly by explicitly marking Node.js built-ins
- Prevents bundlers from attempting to polyfill or bundle these modules
- Reduces bundle size for web applications using Brainy
- Improves tree-shaking and dead code elimination
Benefits for external bundlers:
- Clear distinction between Node.js built-ins and external dependencies
- No ambiguity about what needs polyfilling
- Smaller bundles for browser builds
- Better compatibility with modern bundlers (Webpack 5, Vite, Rollup, esbuild)
🤖 Generated with Claude Code
Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-17 14:20:21 -07:00
const { gzipSync } = await import ( 'node:zlib' )
2025-09-11 16:23:32 -07:00
const jsonString = JSON . stringify ( backupData )
const compressed = gzipSync ( Buffer . from ( jsonString ) )
return {
compressed : true ,
data : compressed.toString ( 'base64' ) ,
originalSize : jsonString.length ,
compressedSize : compressed.length
}
}
return backupData
}
/ * *
* Restore data from a backup
* /
async restore ( params : {
backup : BackupData
merge? : boolean
overwrite? : boolean
validate? : boolean
} ) : Promise < void > {
const { backup , merge = false , overwrite = false , validate = true } = params
// Validate backup format
if ( validate ) {
if ( ! backup . version || ! backup . entities || ! backup . relations ) {
throw new Error ( 'Invalid backup format' )
}
}
// Clear existing data if not merging
if ( ! merge && overwrite ) {
await this . clear ( { entities : true , relations : true } )
}
// Restore entities
for ( const entity of backup . entities ) {
try {
2025-10-17 12:29:27 -07:00
// v4.0.0: Prepare noun and metadata separately
2025-09-11 16:23:32 -07:00
const noun : HNSWNoun = {
id : entity.id ,
vector : entity.vector || new Array ( 384 ) . fill ( 0 ) , // Default vector if missing
connections : new Map ( ) ,
2025-10-17 12:29:27 -07:00
level : 0
}
const metadata = {
. . . entity . metadata ,
noun : entity.type ,
service : entity.service ,
createdAt : Date.now ( )
2025-09-11 16:23:32 -07:00
}
// Check if entity exists when merging
if ( merge ) {
const existing = await this . storage . getNoun ( entity . id )
if ( existing && ! overwrite ) {
continue // Skip existing entities unless overwriting
}
}
await this . storage . saveNoun ( noun )
2025-10-17 12:29:27 -07:00
await this . storage . saveNounMetadata ( entity . id , metadata )
2025-09-11 16:23:32 -07:00
} catch ( error ) {
console . error ( ` Failed to restore entity ${ entity . id } : ` , error )
}
}
// Restore relations
for ( const relation of backup . relations ) {
try {
// Get source and target entities to compute relation vector
const sourceNoun = await this . storage . getNoun ( relation . from )
const targetNoun = await this . storage . getNoun ( relation . to )
if ( ! sourceNoun || ! targetNoun ) {
console . warn ( ` Skipping relation ${ relation . id } : missing entities ` )
continue
}
// Compute relation vector as average of source and target
const relationVector = sourceNoun . vector . map (
( v , i ) = > ( v + targetNoun . vector [ i ] ) / 2
)
2025-10-17 12:29:27 -07:00
// v4.0.0: Prepare verb and metadata separately
const verb = {
2025-09-11 16:23:32 -07:00
id : relation.id ,
vector : relationVector ,
2025-10-17 12:29:27 -07:00
connections : new Map ( ) ,
2025-09-11 16:23:32 -07:00
verb : relation.type as VerbType ,
2025-10-17 12:29:27 -07:00
sourceId : relation.from ,
targetId : relation.to
}
const verbMetadata = {
2025-09-11 16:23:32 -07:00
weight : relation.weight ,
2025-10-17 12:29:27 -07:00
. . . relation . metadata ,
2025-09-11 16:23:32 -07:00
createdAt : Date.now ( )
2025-10-17 12:29:27 -07:00
}
2025-09-11 16:23:32 -07:00
// Check if relation exists when merging
if ( merge ) {
const existing = await this . storage . getVerb ( relation . id )
if ( existing && ! overwrite ) {
continue
}
}
await this . storage . saveVerb ( verb )
2025-10-17 12:29:27 -07:00
await this . storage . saveVerbMetadata ( relation . id , verbMetadata )
2025-09-11 16:23:32 -07:00
} catch ( error ) {
console . error ( ` Failed to restore relation ${ relation . id } : ` , error )
}
}
}
/ * *
* Clear data
* /
async clear ( params : {
entities? : boolean
relations? : boolean
config? : boolean
} = { } ) : Promise < void > {
const { entities = true , relations = true , config = false } = params
if ( entities ) {
// Clear all entities
const nounsResult = await this . storage . getNouns ( {
pagination : { limit : 1000000 }
} )
for ( const noun of nounsResult . items ) {
await this . storage . deleteNoun ( noun . id )
}
// Also clear the HNSW index if available
if ( this . brain ? . index ? . clear ) {
this . brain . index . clear ( )
}
// Clear metadata index if available
if ( this . brain ? . metadataIndex ) {
await this . brain . metadataIndex . rebuild ( ) // Rebuild empty index
}
}
if ( relations ) {
// Clear all relations
const verbsResult = await this . storage . getVerbs ( {
pagination : { limit : 1000000 }
} )
for ( const verb of verbsResult . items ) {
await this . storage . deleteVerb ( verb . id )
}
}
if ( config ) {
// Clear configuration would be handled by ConfigAPI
// For now, skip this
}
}
/ * *
* Import data from various formats
* /
async import ( params : ImportOptions & { data : any } ) : Promise < ImportResult > {
const {
data ,
format ,
mapping = { } ,
batchSize = 100 ,
validate = true
} = params
const result : ImportResult = {
successful : 0 ,
failed : 0 ,
errors : [ ] ,
duration : 0
}
const startTime = Date . now ( )
try {
// ALWAYS use neural import for proper type matching
const { UniversalImportAPI } = await import ( './UniversalImportAPI.js' )
const universalImport = new UniversalImportAPI ( this . brain )
await universalImport . init ( )
// Convert to ImportSource format
const neuralResult = await universalImport . import ( {
type : 'object' ,
data ,
format : format || 'json' ,
metadata : { mapping , batchSize , validate }
} )
// Convert neural result to ImportResult format
result . successful = neuralResult . stats . entitiesCreated
result . failed = 0 // Neural import always succeeds with best match
result . duration = neuralResult . stats . processingTimeMs
// Log relationships created
if ( neuralResult . stats . relationshipsCreated > 0 ) {
console . log ( ` Neural import also created ${ neuralResult . stats . relationshipsCreated } relationships ` )
}
return result
} catch ( error ) {
// Fallback to legacy import ONLY if neural import fails to load
console . warn ( 'Neural import failed, using legacy import:' , error )
let items : any [ ] = [ ]
// Parse data based on format
switch ( format ) {
case 'json' :
items = Array . isArray ( data ) ? data : [ data ]
break
case 'csv' :
// CSV parsing would go here
// For now, assume data is already parsed
items = data
break
// Parquet format removed - not implemented
default :
throw new Error ( ` Unsupported format: ${ format } ` )
}
// Process items in batches
for ( let i = 0 ; i < items . length ; i += batchSize ) {
const batch = items . slice ( i , i + batchSize )
for ( const item of batch ) {
try {
// Apply field mapping
const mapped = this . applyMapping ( item , mapping )
// Validate if requested
if ( validate ) {
this . validateImportItem ( mapped )
}
2025-10-17 12:29:27 -07:00
// v4.0.0: Save entity - separate vector and metadata
const id = mapped . id || this . generateId ( )
2025-09-11 16:23:32 -07:00
const noun : HNSWNoun = {
2025-10-17 12:29:27 -07:00
id ,
2025-09-11 16:23:32 -07:00
vector : mapped.vector || new Array ( 384 ) . fill ( 0 ) ,
connections : new Map ( ) ,
2025-10-17 12:29:27 -07:00
level : 0
2025-09-11 16:23:32 -07:00
}
await this . storage . saveNoun ( noun )
2025-10-17 12:29:27 -07:00
await this . storage . saveNounMetadata ( id , { . . . mapped , createdAt : Date.now ( ) } )
2025-09-11 16:23:32 -07:00
result . successful ++
} catch ( error ) {
result . failed ++
result . errors . push ( {
item ,
error : ( error as Error ) . message
} )
}
}
}
result . duration = Date . now ( ) - startTime
return result
}
}
/ * *
* Export data to various formats
* /
async export ( params : ExportOptions = { } ) : Promise < any > {
const {
format = 'json' ,
filter = { } ,
includeVectors = false
} = params
// Get filtered entities
const nounsResult = await this . storage . getNouns ( {
pagination : { limit : 1000000 }
} )
let entities = nounsResult . items
// Apply filters
if ( filter . type ) {
const types = Array . isArray ( filter . type ) ? filter . type : [ filter . type ]
entities = entities . filter ( e = >
types . includes ( e . metadata ? . noun as NounType )
)
}
if ( filter . service ) {
entities = entities . filter ( e = >
e . metadata ? . service === filter . service
)
}
if ( filter . where ) {
entities = entities . filter ( e = >
this . matchesFilter ( e . metadata , filter . where ! )
)
}
// Format data based on export format
switch ( format ) {
case 'json' :
return entities . map ( e = > ( {
id : e.id ,
vector : includeVectors ? e.vector : undefined ,
. . . e . metadata
} ) )
case 'csv' :
// Convert to CSV format
// For now, return simplified format
return this . convertToCSV ( entities )
// Parquet format removed - not implemented
default :
throw new Error ( ` Unsupported export format: ${ format } ` )
}
}
/ * *
* Get storage statistics
* /
async getStats ( ) : Promise < {
entities : number
relations : number
storageSize? : number
vectorDimensions? : number
} > {
const nounsResult = await this . storage . getNouns ( {
pagination : { limit : 1 }
} )
const verbsResult = await this . storage . getVerbs ( {
pagination : { limit : 1 }
} )
const firstNoun = nounsResult . items [ 0 ]
return {
entities : nounsResult.totalCount || nounsResult . items . length ,
relations : verbsResult.totalCount || verbsResult . items . length ,
vectorDimensions : firstNoun?.vector?.length
}
}
// Helper methods
private applyMapping ( item : any , mapping : Record < string , string > ) : any {
const mapped : any = { }
for ( const [ key , value ] of Object . entries ( item ) ) {
const mappedKey = mapping [ key ] || key
mapped [ mappedKey ] = value
}
return mapped
}
private validateImportItem ( item : any ) : void {
// Basic validation
if ( ! item || typeof item !== 'object' ) {
throw new Error ( 'Invalid item: must be an object' )
}
// Could add more validation here
}
private matchesFilter ( metadata : any , filter : Record < string , any > ) : boolean {
for ( const [ key , value ] of Object . entries ( filter ) ) {
if ( metadata [ key ] !== value ) {
return false
}
}
return true
}
2025-10-17 12:29:27 -07:00
private convertToCSV ( entities : Array < { id : string ; metadata? : any } > ) : string {
2025-09-11 16:23:32 -07:00
if ( entities . length === 0 ) return ''
2025-10-17 12:29:27 -07:00
2025-09-11 16:23:32 -07:00
// Get all unique keys from metadata
const keys = new Set < string > ( )
for ( const entity of entities ) {
if ( entity . metadata ) {
Object . keys ( entity . metadata ) . forEach ( k = > keys . add ( k ) )
}
}
2025-10-17 12:29:27 -07:00
2025-09-11 16:23:32 -07:00
// Create CSV header
const headers = [ 'id' , . . . Array . from ( keys ) ]
const rows = [ headers . join ( ',' ) ]
2025-10-17 12:29:27 -07:00
2025-09-11 16:23:32 -07:00
// Add data rows
for ( const entity of entities ) {
const row = [ entity . id ]
for ( const key of keys ) {
const value = entity . metadata ? . [ key ] || ''
// Escape values that contain commas
2025-10-17 12:29:27 -07:00
const escaped = String ( value ) . includes ( ',' )
? ` " ${ String ( value ) . replace ( /"/g , '""' ) } " `
2025-09-11 16:23:32 -07:00
: String ( value )
row . push ( escaped )
}
rows . push ( row . join ( ',' ) )
}
2025-10-17 12:29:27 -07:00
2025-09-11 16:23:32 -07:00
return rows . join ( '\n' )
}
private generateId ( ) : string {
return ` import_ ${ Date . now ( ) } _ ${ Math . random ( ) . toString ( 36 ) . substr ( 2 , 9 ) } `
}
}