Implements Phase 1.5 (Comprehensive MIME Type Detection) and adds built-in image processing support to IntelligentImportAugmentation. **New Features:** - ImageHandler: Extracts image metadata (dimensions, format, color space) using sharp - EXIF extraction: Camera data, GPS, timestamps using exifr library - Support for JPEG, PNG, WebP, GIF, TIFF, BMP, SVG, HEIC, AVIF formats - MimeTypeDetector: Unified MIME type detection with magic byte support - FormatDetector: Enhanced with image format detection via MIME + magic bytes **Architecture Fixes:** - Fixed brain.import() augmentation pipeline integration (src/brainy.ts:3140-3154) - Added parameter spreading for ImportSource objects to enable augmentation access - Fixed metadata propagation through ImportCoordinator to final results - Added augmentation data check in ImportCoordinator.extract() **Integration:** - ImageHandler registered as built-in handler alongside CSV, Excel, PDF - Images import as 'media' entities with 'image' subtype - Full metadata preserved in knowledge graph entities - Configuration options: enableImage, extractEXIF, imageDefaults **Test Coverage:** - 15 integration tests (image-import.test.ts) - 100% passing - 27 unit tests (image-handler.test.ts) - 100% passing - Format detection tests for all supported image types - Error handling and resilience tests **Breaking Changes:** None - backward compatible Generated with Claude Code Co-Authored-By: Claude <noreply@anthropic.com>
16 KiB
Creating Custom Format Handlers
Version: 5.2.0+
Format handlers enable you to import ANY file type into Brainy as structured knowledge graph data. This guide shows how to create custom format handlers for your specific file formats.
Quick Start
import { BaseFormatHandler, globalHandlerRegistry } from '@soulcraft/brainy/augmentations/intelligentImport'
import type { FormatHandlerOptions, ProcessedData } from '@soulcraft/brainy/augmentations/intelligentImport'
class MyFormatHandler extends BaseFormatHandler {
readonly format = 'myformat'
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
// Option 1: Check by MIME type
if (typeof data === 'object' && 'filename' in data) {
const mimeType = this.getMimeType(data)
return this.mimeTypeMatches(mimeType, ['application/x-myformat'])
}
// Option 2: Check by magic bytes
if (Buffer.isBuffer(data)) {
return data[0] === 0x4D && data[1] === 0x59 // "MY" magic bytes
}
return false
}
async process(
data: Buffer | string,
options: FormatHandlerOptions
): Promise<ProcessedData> {
// Convert to Buffer
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
// Parse your format
const parsed = this.parseMyFormat(buffer)
// Return structured data
return {
format: 'myformat',
data: [
{
type: 'MyEntity',
name: parsed.name,
metadata: parsed.metadata
}
],
metadata: {
rowCount: 1,
fields: ['type', 'name', 'metadata'],
processingTime: Date.now() - startTime
}
}
}
private parseMyFormat(buffer: Buffer): any {
// Your parsing logic here
return { name: 'example', metadata: {} }
}
}
// Register globally
globalHandlerRegistry.registerHandler({
name: 'myformat',
mimeTypes: ['application/x-myformat'],
extensions: ['.myf', '.myfmt'],
loader: async () => new MyFormatHandler()
})
Now Brainy automatically handles your format:
await brain.import({
type: 'file',
data: myFormatBuffer,
filename: 'data.myf'
})
// Automatically routes to MyFormatHandler!
BaseFormatHandler
All format handlers should extend BaseFormatHandler, which provides:
MIME Type Detection
protected getMimeType(data: Buffer | string | { filename?: string }): string
Detects MIME type from filename or buffer. Uses Brainy's comprehensive MIME detection (2000+ types).
Example:
const mimeType = this.getMimeType({ filename: 'data.dwg' })
// Returns: 'image/vnd.dwg'
MIME Type Matching
protected mimeTypeMatches(mimeType: string, patterns: string[]): boolean
Checks if MIME type matches patterns. Supports wildcards (image/*).
Example:
if (this.mimeTypeMatches(mimeType, ['image/*', 'video/*'])) {
// Handle all images and videos
}
Extension Detection
protected detectExtension(data: string | Buffer | { filename?: string; ext?: string }): string | null
Extracts file extension for fallback detection.
canHandle() Method
The canHandle() method determines if your handler can process the given data.
Strategy 1: MIME Type Detection (Recommended)
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
if (typeof data === 'object' && 'filename' in data) {
const mimeType = this.getMimeType(data)
return this.mimeTypeMatches(mimeType, [
'application/x-myformat',
'application/myformat'
])
}
return false
}
✅ Pros: Automatic, comprehensive, works with 2000+ types ❌ Cons: Requires filename
Strategy 2: Magic Byte Detection
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
if (Buffer.isBuffer(data)) {
// Check magic bytes
return (
data[0] === 0x50 && // 'P'
data[1] === 0x4B && // 'K'
data[2] === 0x03 &&
data[3] === 0x04 // ZIP signature
)
}
return false
}
✅ Pros: Works without filename, robust ❌ Cons: Requires knowledge of format structure
Strategy 3: Combined Approach
canHandle(data: Buffer | string | { filename?: string; ext?: string }): boolean {
// Try MIME type first
if (typeof data === 'object' && 'filename' in data) {
const mimeType = this.getMimeType(data)
if (this.mimeTypeMatches(mimeType, ['application/x-myformat'])) {
return true
}
}
// Fallback to magic bytes
if (Buffer.isBuffer(data)) {
return this.checkMagicBytes(data)
}
return false
}
✅ Pros: Robust, works in all scenarios ❌ Cons: More complex
process() Method
The process() method extracts structured data from the file.
Return Format
interface ProcessedData {
/** Format identifier */
format: string
/** Array of extracted entities */
data: Array<Record<string, any>>
/** Metadata about processing */
metadata: {
rowCount: number
fields: string[]
processingTime: number
[key: string]: any
}
/** Original filename (optional) */
filename?: string
}
Example: CAD File Handler
async process(data: Buffer | string, options: FormatHandlerOptions): Promise<ProcessedData> {
const startTime = Date.now()
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
// Parse CAD file
const cad = await this.parseCAD(buffer)
// Extract entities
const entities = []
// Main CAD document
entities.push({
type: 'CADDocument',
filename: options.filename,
units: cad.units,
bounds: cad.bounds
})
// Layers
for (const layer of cad.layers) {
entities.push({
type: 'CADLayer',
name: layer.name,
color: layer.color,
visible: layer.visible,
objectCount: layer.objects.length
})
}
// Objects
for (const obj of cad.objects) {
entities.push({
type: 'CADObject',
objectType: obj.type,
layer: obj.layer,
geometry: obj.geometry,
properties: obj.properties
})
}
return {
format: 'cad',
data: entities,
metadata: {
rowCount: entities.length,
fields: ['type', 'name', 'geometry', 'properties'],
processingTime: Date.now() - startTime,
layerCount: cad.layers.length,
objectCount: cad.objects.length,
units: cad.units
},
filename: options.filename
}
}
Best Practices
-
Always track processing time:
const startTime = Date.now() // ... processing ... metadata.processingTime = Date.now() - startTime -
Include rich metadata:
metadata: { rowCount: entities.length, fields: ['type', 'name', ...], processingTime: 123, // Format-specific metadata layerCount: 5, objectCount: 150, version: '2.0' } -
Handle errors gracefully:
try { const parsed = this.parse(buffer) return { format: 'myformat', data: parsed, ... } } catch (error) { throw new Error( `Failed to parse myformat: ${error instanceof Error ? error.message : String(error)}` ) } -
Support progress reporting (optional):
if (options.progressHooks?.onCurrentItem) { options.progressHooks.onCurrentItem(`Processing layer ${i}/${total}`) }
FormatHandlerRegistry
Global Registry
Use the global registry for application-wide handlers:
import { globalHandlerRegistry } from '@soulcraft/brainy/augmentations/intelligentImport'
globalHandlerRegistry.registerHandler({
name: 'myformat',
mimeTypes: ['application/x-myformat'],
extensions: ['.myf'],
loader: async () => new MyFormatHandler()
})
Local Registry
Create a local registry for scoped handlers:
import { FormatHandlerRegistry } from '@soulcraft/brainy/augmentations/intelligentImport'
const registry = new FormatHandlerRegistry()
registry.registerHandler({ ... })
Lazy Loading
Handlers are lazy-loaded for performance:
globalHandlerRegistry.registerHandler({
name: 'heavy',
mimeTypes: ['application/x-heavy'],
extensions: ['.heavy'],
loader: async () => {
// Only loaded when first needed
const { HeavyHandler } = await import('./HeavyHandler.js')
return new HeavyHandler()
}
})
Getting Handlers
// By filename (automatic MIME detection)
const handler = await registry.getHandler('data.myf')
// By MIME type
const handler = await registry.getHandlerByMimeType('application/x-myformat')
// By extension
const handler = await registry.getHandlerByExtension('.myf')
// By name
const handler = await registry.getHandlerByName('myformat')
Real-World Examples
Example 1: Video Metadata Extractor
import { BaseFormatHandler } from '@soulcraft/brainy/augmentations/intelligentImport'
import ffmpeg from 'fluent-ffmpeg'
class VideoHandler extends BaseFormatHandler {
readonly format = 'video'
canHandle(data) {
const mimeType = this.getMimeType(data)
return this.mimeTypeMatches(mimeType, ['video/*'])
}
async process(data, options) {
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
// Extract video metadata with ffmpeg
const metadata = await this.extractVideoMetadata(buffer)
return {
format: 'video',
data: [{
type: 'Video',
duration: metadata.duration,
codec: metadata.codec,
resolution: metadata.resolution,
frameRate: metadata.frameRate,
bitrate: metadata.bitrate,
audioTracks: metadata.audioTracks,
subtitles: metadata.subtitles
}],
metadata: {
rowCount: 1,
fields: ['type', 'duration', 'codec', 'resolution'],
processingTime: metadata.processingTime
}
}
}
private async extractVideoMetadata(buffer: Buffer) {
// Use ffmpeg to extract metadata
return new Promise((resolve, reject) => {
ffmpeg(buffer)
.ffprobe((err, data) => {
if (err) reject(err)
else resolve(this.parseFFmpegOutput(data))
})
})
}
}
Example 2: Git Repository Parser
import { BaseFormatHandler } from '@soulcraft/brainy/augmentations/intelligentImport'
import { simpleGit } from 'simple-git'
class GitRepoHandler extends BaseFormatHandler {
readonly format = 'git-repo'
canHandle(data) {
// Check for .git directory
if (typeof data === 'object' && 'filename' in data) {
return data.filename?.includes('.git') || false
}
return false
}
async process(data, options) {
const repoPath = options.filename || ''
const git = simpleGit(repoPath)
// Extract commits
const log = await git.log()
const commits = log.all.map(commit => ({
type: 'GitCommit',
hash: commit.hash,
message: commit.message,
author: commit.author_name,
date: commit.date
}))
// Extract branches
const branchSummary = await git.branchLocal()
const branches = Object.keys(branchSummary.branches).map(name => ({
type: 'GitBranch',
name,
current: branchSummary.current === name
}))
return {
format: 'git-repo',
data: [...commits, ...branches],
metadata: {
rowCount: commits.length + branches.length,
fields: ['type', 'hash', 'message', 'author'],
processingTime: Date.now() - startTime,
commitCount: commits.length,
branchCount: branches.length
}
}
}
}
Example 3: Database Schema Importer
import { BaseFormatHandler } from '@soulcraft/brainy/augmentations/intelligentImport'
import { Client } from 'pg'
class PostgreSQLSchemaHandler extends BaseFormatHandler {
readonly format = 'postgres-schema'
canHandle(data) {
if (typeof data === 'object' && 'filename' in data) {
return data.filename?.endsWith('.sql') || false
}
return false
}
async process(data, options) {
const buffer = Buffer.isBuffer(data) ? data : Buffer.from(data)
const sql = buffer.toString('utf-8')
// Parse SQL or connect to database
const schema = await this.parseSchema(sql)
const entities = []
// Tables
for (const table of schema.tables) {
entities.push({
type: 'Table',
name: table.name,
schema: table.schema,
columnCount: table.columns.length
})
// Columns
for (const column of table.columns) {
entities.push({
type: 'Column',
name: column.name,
table: table.name,
dataType: column.dataType,
nullable: column.nullable,
primaryKey: column.primaryKey
})
}
}
// Foreign keys
for (const fk of schema.foreignKeys) {
entities.push({
type: 'ForeignKey',
from: `${fk.fromTable}.${fk.fromColumn}`,
to: `${fk.toTable}.${fk.toColumn}`
})
}
return {
format: 'postgres-schema',
data: entities,
metadata: {
rowCount: entities.length,
fields: ['type', 'name', 'table', 'dataType'],
processingTime: Date.now() - startTime,
tableCount: schema.tables.length,
columnCount: schema.tables.reduce((sum, t) => sum + t.columns.length, 0),
foreignKeyCount: schema.foreignKeys.length
}
}
}
}
Creating Premium Augmentations
Package your handler as a premium augmentation:
// @yourcompany/brainy-cad-importer
import { BaseAugmentation } from '@soulcraft/brainy'
import { CADHandler } from './CADHandler.js'
export class CADImportAugmentation extends BaseAugmentation {
readonly name = 'cad-import'
readonly timing = 'before'
readonly operations = ['import', 'importFile']
private handler: CADHandler
constructor(config = {}) {
super(config)
this.handler = new CADHandler()
}
async execute(operation, params, next) {
// Check if this is a CAD file
if (this.isCADFile(params)) {
const processed = await this.handler.process(params.data, params.options)
params.data = processed.data
params.metadata = { ...params.metadata, ...processed.metadata }
}
return next()
}
private isCADFile(params: any): boolean {
return this.handler.canHandle(params.data || params)
}
}
// Usage:
// npm install @yourcompany/brainy-cad-importer
// brain.addAugmentation(new CADImportAugmentation())
Testing
import { describe, it, expect } from 'vitest'
import { MyFormatHandler } from './MyFormatHandler.js'
describe('MyFormatHandler', () => {
let handler: MyFormatHandler
beforeEach(() => {
handler = new MyFormatHandler()
})
describe('canHandle', () => {
it('should handle .myf files', () => {
expect(handler.canHandle({ filename: 'data.myf' })).toBe(true)
})
it('should handle by MIME type', () => {
expect(handler.canHandle({ filename: 'data.myformat' })).toBe(true)
})
it('should reject non-myformat files', () => {
expect(handler.canHandle({ filename: 'data.txt' })).toBe(false)
})
})
describe('process', () => {
it('should extract structured data', async () => {
const testData = Buffer.from('MY format data')
const result = await handler.process(testData)
expect(result.format).toBe('myformat')
expect(result.data).toHaveLength(1)
expect(result.metadata.processingTime).toBeGreaterThan(0)
})
it('should handle errors gracefully', async () => {
const invalidData = Buffer.from('invalid')
await expect(handler.process(invalidData)).rejects.toThrow()
})
})
})
Best Practices
- Always extend BaseFormatHandler - provides MIME detection and utilities
- Use MIME types for routing - automatic, comprehensive, maintainable
- Lazy load heavy dependencies - better performance
- Extract rich metadata - make data queryable in knowledge graph
- Handle errors gracefully - fail fast with clear error messages
- Test thoroughly - test canHandle() and process() with real data
- Document your format - explain what data is extracted and how
- Follow ProcessedData format - ensures compatibility with Brainy
See Also
- ImageHandler source - Reference implementation
- BaseFormatHandler source - Base class
- FormatHandlerRegistry source - Registry implementation
- Augmentations Guide - Creating augmentations