feat: Integration Hub for external tool connectivity
- Add native config option: `new Brainy({ integrations: true })`
- OData integration for Excel Power Query and Power BI
- Google Sheets integration with Apps Script
- Server-Sent Events (SSE) for real-time streaming
- Webhooks for push notifications
- Zero-config with sensible defaults
- Full tree-shaking when disabled
This commit is contained in:
parent
24039e8a1a
commit
b5bc9000cf
28 changed files with 8186 additions and 1 deletions
313
src/integrations/core/EventBus.ts
Normal file
313
src/integrations/core/EventBus.ts
Normal file
|
|
@ -0,0 +1,313 @@
|
|||
/**
|
||||
* Integration Hub - Event Bus
|
||||
*
|
||||
* Central event emitter for real-time change propagation.
|
||||
* Enables integrations to react to Brainy data changes.
|
||||
*/
|
||||
|
||||
import {
|
||||
BrainyEvent,
|
||||
EventFilter,
|
||||
EventHandler,
|
||||
EventSubscription
|
||||
} from './types.js'
|
||||
import { Entity, Relation } from '../../types/brainy.types.js'
|
||||
import { NounType, VerbType } from '../../types/graphTypes.js'
|
||||
|
||||
/**
|
||||
* Central event bus for real-time Brainy events
|
||||
*
|
||||
* Features:
|
||||
* - Pub/sub pattern for event distribution
|
||||
* - Filtering by entity type, operation, noun/verb types
|
||||
* - Sequence IDs for ordering and resumption
|
||||
* - Optional event buffering for batch processing
|
||||
* - Memory-efficient circular buffer for replay
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const eventBus = new EventBus()
|
||||
*
|
||||
* // Subscribe to all noun creates
|
||||
* eventBus.subscribe(
|
||||
* { entityTypes: ['noun'], operations: ['create'] },
|
||||
* (event) => console.log('New entity:', event.entityId)
|
||||
* )
|
||||
*
|
||||
* // Emit event
|
||||
* eventBus.emit({
|
||||
* entityType: 'noun',
|
||||
* operation: 'create',
|
||||
* entityId: 'entity-123',
|
||||
* nounType: NounType.Person
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class EventBus {
|
||||
private subscriptions: Map<
|
||||
string,
|
||||
{ filter: EventFilter; handler: EventHandler }
|
||||
> = new Map()
|
||||
private sequenceCounter: bigint = 0n
|
||||
private eventBuffer: BrainyEvent[] = []
|
||||
private bufferSize: number
|
||||
private subscriptionIdCounter = 0
|
||||
|
||||
/**
|
||||
* Create a new EventBus
|
||||
*
|
||||
* @param options Configuration options
|
||||
* @param options.bufferSize Size of replay buffer (default: 1000)
|
||||
*/
|
||||
constructor(options: { bufferSize?: number } = {}) {
|
||||
this.bufferSize = options.bufferSize ?? 1000
|
||||
}
|
||||
|
||||
/**
|
||||
* Subscribe to events matching a filter
|
||||
*
|
||||
* @param filter Event filter criteria
|
||||
* @param handler Function to call when matching events occur
|
||||
* @returns Subscription that can be used to unsubscribe
|
||||
*/
|
||||
subscribe(filter: EventFilter, handler: EventHandler): EventSubscription {
|
||||
const id = `sub-${++this.subscriptionIdCounter}`
|
||||
|
||||
this.subscriptions.set(id, { filter, handler })
|
||||
|
||||
// If filter has 'since', replay buffered events
|
||||
if (filter.since !== undefined) {
|
||||
this.replayEvents(filter, handler)
|
||||
}
|
||||
|
||||
return {
|
||||
id,
|
||||
unsubscribe: () => {
|
||||
this.subscriptions.delete(id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit an event to all matching subscribers
|
||||
*
|
||||
* @param partialEvent Event data (id, timestamp, sequenceId auto-generated)
|
||||
*/
|
||||
emit(
|
||||
partialEvent: Omit<BrainyEvent, 'id' | 'timestamp' | 'sequenceId'>
|
||||
): BrainyEvent {
|
||||
const event: BrainyEvent = {
|
||||
...partialEvent,
|
||||
id: this.generateEventId(),
|
||||
timestamp: Date.now(),
|
||||
sequenceId: ++this.sequenceCounter
|
||||
}
|
||||
|
||||
// Add to buffer
|
||||
this.addToBuffer(event)
|
||||
|
||||
// Dispatch to matching subscribers
|
||||
this.dispatch(event)
|
||||
|
||||
return event
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a noun event
|
||||
*/
|
||||
emitNoun(
|
||||
operation: 'create' | 'update' | 'delete',
|
||||
entityId: string,
|
||||
nounType: NounType,
|
||||
options?: { service?: string; data?: Entity }
|
||||
): BrainyEvent {
|
||||
return this.emit({
|
||||
entityType: 'noun',
|
||||
operation,
|
||||
entityId,
|
||||
nounType,
|
||||
service: options?.service,
|
||||
data: options?.data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a verb/relation event
|
||||
*/
|
||||
emitVerb(
|
||||
operation: 'create' | 'update' | 'delete',
|
||||
entityId: string,
|
||||
verbType: VerbType,
|
||||
options?: { service?: string; data?: Relation }
|
||||
): BrainyEvent {
|
||||
return this.emit({
|
||||
entityType: 'verb',
|
||||
operation,
|
||||
entityId,
|
||||
verbType,
|
||||
service: options?.service,
|
||||
data: options?.data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Emit a VFS event
|
||||
*/
|
||||
emitVFS(
|
||||
operation: 'create' | 'update' | 'delete',
|
||||
entityId: string,
|
||||
options?: { service?: string; data?: Entity }
|
||||
): BrainyEvent {
|
||||
return this.emit({
|
||||
entityType: 'vfs',
|
||||
operation,
|
||||
entityId,
|
||||
service: options?.service,
|
||||
data: options?.data
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current sequence ID for resumption
|
||||
*/
|
||||
getCurrentSequenceId(): bigint {
|
||||
return this.sequenceCounter
|
||||
}
|
||||
|
||||
/**
|
||||
* Get events since a sequence ID (from buffer)
|
||||
*/
|
||||
getEventsSince(sequenceId: bigint): BrainyEvent[] {
|
||||
return this.eventBuffer.filter((event) => event.sequenceId > sequenceId)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get subscription count
|
||||
*/
|
||||
getSubscriptionCount(): number {
|
||||
return this.subscriptions.size
|
||||
}
|
||||
|
||||
/**
|
||||
* Clear all subscriptions
|
||||
*/
|
||||
clear(): void {
|
||||
this.subscriptions.clear()
|
||||
this.eventBuffer = []
|
||||
this.sequenceCounter = 0n
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an event matches a filter
|
||||
*/
|
||||
private matchesFilter(event: BrainyEvent, filter: EventFilter): boolean {
|
||||
// Check entity types
|
||||
if (
|
||||
filter.entityTypes &&
|
||||
filter.entityTypes.length > 0 &&
|
||||
!filter.entityTypes.includes(event.entityType)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check operations
|
||||
if (
|
||||
filter.operations &&
|
||||
filter.operations.length > 0 &&
|
||||
!filter.operations.includes(event.operation)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check noun types
|
||||
if (
|
||||
filter.nounTypes &&
|
||||
filter.nounTypes.length > 0 &&
|
||||
event.nounType &&
|
||||
!filter.nounTypes.includes(event.nounType)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check verb types
|
||||
if (
|
||||
filter.verbTypes &&
|
||||
filter.verbTypes.length > 0 &&
|
||||
event.verbType &&
|
||||
!filter.verbTypes.includes(event.verbType)
|
||||
) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check service
|
||||
if (filter.service && event.service !== filter.service) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Check sequence ID
|
||||
if (filter.since !== undefined && event.sequenceId <= filter.since) {
|
||||
return false
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Dispatch event to matching subscribers
|
||||
*/
|
||||
private async dispatch(event: BrainyEvent): Promise<void> {
|
||||
const promises: Promise<void>[] = []
|
||||
|
||||
for (const [_, subscription] of this.subscriptions) {
|
||||
if (this.matchesFilter(event, subscription.filter)) {
|
||||
const result = subscription.handler(event)
|
||||
if (result instanceof Promise) {
|
||||
promises.push(result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Wait for all async handlers (fire and forget for sync handlers)
|
||||
if (promises.length > 0) {
|
||||
await Promise.allSettled(promises)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Replay buffered events to a new subscriber
|
||||
*/
|
||||
private async replayEvents(
|
||||
filter: EventFilter,
|
||||
handler: EventHandler
|
||||
): Promise<void> {
|
||||
const eventsToReplay = this.eventBuffer.filter((event) =>
|
||||
this.matchesFilter(event, filter)
|
||||
)
|
||||
|
||||
for (const event of eventsToReplay) {
|
||||
const result = handler(event)
|
||||
if (result instanceof Promise) {
|
||||
await result
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Add event to circular buffer
|
||||
*/
|
||||
private addToBuffer(event: BrainyEvent): void {
|
||||
this.eventBuffer.push(event)
|
||||
|
||||
// Maintain buffer size
|
||||
while (this.eventBuffer.length > this.bufferSize) {
|
||||
this.eventBuffer.shift()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Generate unique event ID
|
||||
*/
|
||||
private generateEventId(): string {
|
||||
return `evt-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 9)}`
|
||||
}
|
||||
}
|
||||
425
src/integrations/core/IntegrationBase.ts
Normal file
425
src/integrations/core/IntegrationBase.ts
Normal file
|
|
@ -0,0 +1,425 @@
|
|||
/**
|
||||
* Integration Hub - Integration Base Class
|
||||
*
|
||||
* Base class for all integration augmentations. Provides common functionality
|
||||
* for event subscriptions, tabular export, and lifecycle management.
|
||||
*/
|
||||
|
||||
import {
|
||||
BaseAugmentation,
|
||||
AugmentationContext
|
||||
} from '../../augmentations/brainyAugmentation.js'
|
||||
import { AugmentationManifest } from '../../augmentations/manifest.js'
|
||||
import { EventBus } from './EventBus.js'
|
||||
import { TabularExporter } from './TabularExporter.js'
|
||||
import {
|
||||
EventFilter,
|
||||
EventHandler,
|
||||
EventSubscription,
|
||||
IntegrationConfig,
|
||||
IntegrationHealthStatus,
|
||||
TabularExporterConfig
|
||||
} from './types.js'
|
||||
import { Entity, Relation, FindParams } from '../../types/brainy.types.js'
|
||||
|
||||
/**
|
||||
* Base class for all integration augmentations
|
||||
*
|
||||
* Provides:
|
||||
* - Shared EventBus for real-time updates
|
||||
* - TabularExporter for entity-to-row conversion
|
||||
* - Common lifecycle methods (start/stop)
|
||||
* - Health monitoring
|
||||
* - Standard configuration patterns
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* class MySQLIntegration extends IntegrationBase {
|
||||
* readonly name = 'sql'
|
||||
* readonly category = 'integration'
|
||||
*
|
||||
* protected async onStart(): Promise<void> {
|
||||
* // Start SQL server
|
||||
* }
|
||||
*
|
||||
* protected async onStop(): Promise<void> {
|
||||
* // Stop SQL server
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export abstract class IntegrationBase extends BaseAugmentation {
|
||||
// Augmentation interface implementation
|
||||
readonly timing = 'after' as const
|
||||
readonly metadata = 'readonly' as const
|
||||
readonly operations: ('all')[] = ['all']
|
||||
readonly priority = 5 // Low priority - runs after main operations
|
||||
category: 'internal' | 'core' | 'premium' | 'community' | 'external' = 'core'
|
||||
|
||||
// Shared infrastructure
|
||||
protected eventBus: EventBus
|
||||
protected exporter: TabularExporter
|
||||
|
||||
// Integration state
|
||||
protected isRunning = false
|
||||
protected startedAt?: number
|
||||
protected requestCount = 0
|
||||
protected errorCount = 0
|
||||
protected lastError?: string
|
||||
|
||||
// Event subscriptions managed by this integration
|
||||
private managedSubscriptions: EventSubscription[] = []
|
||||
|
||||
/**
|
||||
* Create a new integration
|
||||
*
|
||||
* @param config Integration configuration
|
||||
* @param exporterConfig Optional TabularExporter configuration
|
||||
*/
|
||||
constructor(
|
||||
config?: IntegrationConfig,
|
||||
exporterConfig?: TabularExporterConfig
|
||||
) {
|
||||
super(config)
|
||||
this.eventBus = new EventBus()
|
||||
this.exporter = new TabularExporter(exporterConfig)
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration name (must be unique)
|
||||
*/
|
||||
abstract readonly name: string
|
||||
|
||||
/**
|
||||
* Start the integration (implement in subclass)
|
||||
*/
|
||||
protected abstract onStart(): Promise<void>
|
||||
|
||||
/**
|
||||
* Stop the integration (implement in subclass)
|
||||
*/
|
||||
protected abstract onStop(): Promise<void>
|
||||
|
||||
// BaseAugmentation lifecycle integration
|
||||
|
||||
protected async onInitialize(): Promise<void> {
|
||||
// Auto-start if enabled
|
||||
if (this.config.enabled !== false) {
|
||||
await this.start()
|
||||
}
|
||||
}
|
||||
|
||||
protected async onShutdown(): Promise<void> {
|
||||
await this.stop()
|
||||
}
|
||||
|
||||
/**
|
||||
* Execute augmentation (intercept operations to emit events)
|
||||
*/
|
||||
async execute<T>(
|
||||
operation: string,
|
||||
params: any,
|
||||
next: () => Promise<T>
|
||||
): Promise<T> {
|
||||
// Execute main operation first
|
||||
const result = await next()
|
||||
|
||||
// Emit events for data-changing operations
|
||||
if (this.isRunning) {
|
||||
this.emitOperationEvent(operation, params, result)
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
// Public API
|
||||
|
||||
/**
|
||||
* Start the integration
|
||||
*/
|
||||
async start(): Promise<void> {
|
||||
if (this.isRunning) {
|
||||
return
|
||||
}
|
||||
|
||||
this.log(`Starting ${this.name} integration...`)
|
||||
|
||||
try {
|
||||
await this.onStart()
|
||||
this.isRunning = true
|
||||
this.startedAt = Date.now()
|
||||
this.log(`${this.name} integration started`)
|
||||
} catch (error: any) {
|
||||
this.lastError = error.message
|
||||
this.log(`Failed to start ${this.name}: ${error.message}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop the integration
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
if (!this.isRunning) {
|
||||
return
|
||||
}
|
||||
|
||||
this.log(`Stopping ${this.name} integration...`)
|
||||
|
||||
try {
|
||||
// Unsubscribe all managed subscriptions
|
||||
for (const sub of this.managedSubscriptions) {
|
||||
sub.unsubscribe()
|
||||
}
|
||||
this.managedSubscriptions = []
|
||||
|
||||
await this.onStop()
|
||||
this.isRunning = false
|
||||
this.log(`${this.name} integration stopped`)
|
||||
} catch (error: any) {
|
||||
this.lastError = error.message
|
||||
this.log(`Error stopping ${this.name}: ${error.message}`, 'error')
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if integration is running
|
||||
*/
|
||||
running(): boolean {
|
||||
return this.isRunning
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health status
|
||||
*/
|
||||
health(): IntegrationHealthStatus {
|
||||
return {
|
||||
name: this.name,
|
||||
status: this.isRunning
|
||||
? this.errorCount > 10
|
||||
? 'degraded'
|
||||
: 'healthy'
|
||||
: 'stopped',
|
||||
message: this.isRunning ? 'Running' : 'Stopped',
|
||||
uptimeMs: this.startedAt ? Date.now() - this.startedAt : undefined,
|
||||
requestCount: this.requestCount,
|
||||
errorCount: this.errorCount,
|
||||
lastError: this.lastError,
|
||||
checkedAt: Date.now()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the shared EventBus
|
||||
*/
|
||||
getEventBus(): EventBus {
|
||||
return this.eventBus
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the TabularExporter
|
||||
*/
|
||||
getExporter(): TabularExporter {
|
||||
return this.exporter
|
||||
}
|
||||
|
||||
// Protected helpers for subclasses
|
||||
|
||||
/**
|
||||
* Subscribe to Brainy events (auto-unsubscribed on stop)
|
||||
*/
|
||||
protected subscribeToChanges(
|
||||
filter: EventFilter,
|
||||
handler: EventHandler
|
||||
): EventSubscription {
|
||||
const subscription = this.eventBus.subscribe(filter, handler)
|
||||
this.managedSubscriptions.push(subscription)
|
||||
return subscription
|
||||
}
|
||||
|
||||
/**
|
||||
* Query entities using Brainy find()
|
||||
*/
|
||||
protected async queryEntities(params: FindParams): Promise<Entity[]> {
|
||||
if (!this.context) {
|
||||
throw new Error('Integration not initialized')
|
||||
}
|
||||
|
||||
const results = await this.context.brain.find(params)
|
||||
return results.map((r: any) => r.entity)
|
||||
}
|
||||
|
||||
/**
|
||||
* Query relations using Brainy getRelations()
|
||||
*/
|
||||
protected async queryRelations(params?: {
|
||||
from?: string
|
||||
to?: string
|
||||
type?: any
|
||||
limit?: number
|
||||
offset?: number
|
||||
}): Promise<Relation[]> {
|
||||
if (!this.context) {
|
||||
throw new Error('Integration not initialized')
|
||||
}
|
||||
|
||||
return this.context.brain.getRelations(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a single entity by ID
|
||||
*/
|
||||
protected async getEntity(id: string): Promise<Entity | null> {
|
||||
if (!this.context) {
|
||||
throw new Error('Integration not initialized')
|
||||
}
|
||||
|
||||
return this.context.brain.get(id)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export entities to tabular format
|
||||
*/
|
||||
protected exportEntities(entities: Entity[]): any[] {
|
||||
return this.exporter.entitiesToRows(entities)
|
||||
}
|
||||
|
||||
/**
|
||||
* Export relations to tabular format
|
||||
*/
|
||||
protected exportRelations(relations: Relation[]): any[] {
|
||||
return this.exporter.relationsToRows(relations)
|
||||
}
|
||||
|
||||
/**
|
||||
* Record a successful request
|
||||
*/
|
||||
protected recordRequest(): void {
|
||||
this.requestCount++
|
||||
}
|
||||
|
||||
/**
|
||||
* Record an error
|
||||
*/
|
||||
protected recordError(error: Error | string): void {
|
||||
this.errorCount++
|
||||
this.lastError = typeof error === 'string' ? error : error.message
|
||||
}
|
||||
|
||||
/**
|
||||
* Get manifest for this integration
|
||||
* Subclasses should override this
|
||||
*/
|
||||
getManifest(): AugmentationManifest {
|
||||
return {
|
||||
id: this.name,
|
||||
name: this.name,
|
||||
version: '1.0.0',
|
||||
description: `${this.name} integration`,
|
||||
category: 'integration',
|
||||
status: 'stable',
|
||||
configSchema: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: {
|
||||
type: 'boolean',
|
||||
default: true,
|
||||
description: 'Whether the integration is enabled'
|
||||
}
|
||||
}
|
||||
},
|
||||
configDefaults: {
|
||||
enabled: true
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Private helpers
|
||||
|
||||
/**
|
||||
* Emit events based on operation type
|
||||
*/
|
||||
private emitOperationEvent(operation: string, params: any, result: any): void {
|
||||
// Map operations to event types
|
||||
const opMap: Record<
|
||||
string,
|
||||
{ entityType: 'noun' | 'verb' | 'vfs'; op: 'create' | 'update' | 'delete' }
|
||||
> = {
|
||||
add: { entityType: 'noun', op: 'create' },
|
||||
addNoun: { entityType: 'noun', op: 'create' },
|
||||
update: { entityType: 'noun', op: 'update' },
|
||||
delete: { entityType: 'noun', op: 'delete' },
|
||||
relate: { entityType: 'verb', op: 'create' },
|
||||
addVerb: { entityType: 'verb', op: 'create' },
|
||||
unrelate: { entityType: 'verb', op: 'delete' },
|
||||
deleteVerb: { entityType: 'verb', op: 'delete' }
|
||||
}
|
||||
|
||||
const mapping = opMap[operation]
|
||||
if (!mapping) {
|
||||
return
|
||||
}
|
||||
|
||||
// Extract entity ID from result or params
|
||||
let entityId = result?.id || params?.id
|
||||
if (!entityId && Array.isArray(result)) {
|
||||
// Batch operation - emit for each
|
||||
for (const item of result) {
|
||||
if (item?.id) {
|
||||
this.eventBus.emit({
|
||||
entityType: mapping.entityType,
|
||||
operation: mapping.op,
|
||||
entityId: item.id,
|
||||
nounType: params?.type || item?.type,
|
||||
verbType: params?.type || item?.type,
|
||||
service: params?.service || item?.service,
|
||||
data: item
|
||||
})
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
if (entityId) {
|
||||
this.eventBus.emit({
|
||||
entityType: mapping.entityType,
|
||||
operation: mapping.op,
|
||||
entityId,
|
||||
nounType: params?.type,
|
||||
verbType: params?.type,
|
||||
service: params?.service,
|
||||
data: result
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for integrations that expose HTTP endpoints
|
||||
*/
|
||||
export interface HTTPIntegration {
|
||||
/** Port the server is listening on */
|
||||
port: number
|
||||
|
||||
/** Base path for routes */
|
||||
basePath: string
|
||||
|
||||
/** Get registered routes */
|
||||
getRoutes(): Array<{
|
||||
method: string
|
||||
path: string
|
||||
description: string
|
||||
}>
|
||||
}
|
||||
|
||||
/**
|
||||
* Interface for integrations that support streaming
|
||||
*/
|
||||
export interface StreamingIntegration {
|
||||
/** Subscribe to real-time events via callback */
|
||||
stream(
|
||||
filter: EventFilter,
|
||||
callback: (event: any) => void
|
||||
): { close: () => void }
|
||||
}
|
||||
368
src/integrations/core/IntegrationHub.ts
Normal file
368
src/integrations/core/IntegrationHub.ts
Normal file
|
|
@ -0,0 +1,368 @@
|
|||
/**
|
||||
* Integration Hub - Zero-Config Integration Manager
|
||||
*
|
||||
* The simplest way to enable external tool integrations.
|
||||
* One line of code, all integrations ready.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Zero-config: Enable all integrations
|
||||
* const hub = await IntegrationHub.create(brain)
|
||||
*
|
||||
* // Get your endpoints
|
||||
* console.log(hub.endpoints)
|
||||
* // {
|
||||
* // odata: '/odata', → Excel, Power BI, Tableau
|
||||
* // sheets: '/sheets', → Google Sheets
|
||||
* // sse: '/events', → Real-time streaming
|
||||
* // webhooks: '/webhooks' → Push notifications
|
||||
* // }
|
||||
* ```
|
||||
*/
|
||||
|
||||
import { IntegrationBase } from './IntegrationBase.js'
|
||||
import { IntegrationLoader, IntegrationType, INTEGRATION_CATALOG } from './IntegrationLoader.js'
|
||||
import { IntegrationConfig, IntegrationHealthStatus } from './types.js'
|
||||
import { ODataIntegration } from '../odata/ODataIntegration.js'
|
||||
import { GoogleSheetsIntegration } from '../sheets/GoogleSheetsIntegration.js'
|
||||
import { SSEIntegration } from '../sse/SSEIntegration.js'
|
||||
import { WebhookIntegration } from '../webhooks/WebhookIntegration.js'
|
||||
|
||||
/**
|
||||
* Integration Hub configuration
|
||||
*/
|
||||
export interface IntegrationHubConfig {
|
||||
/** Base path for all endpoints (default: '') */
|
||||
basePath?: string
|
||||
|
||||
/** Which integrations to enable (default: all) */
|
||||
enable?: IntegrationType[] | 'all'
|
||||
|
||||
/** Per-integration config overrides */
|
||||
config?: {
|
||||
odata?: IntegrationConfig & { basePath?: string }
|
||||
sheets?: IntegrationConfig & { basePath?: string }
|
||||
sse?: IntegrationConfig & { basePath?: string }
|
||||
webhooks?: IntegrationConfig & { basePath?: string }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP request for integration routing
|
||||
*/
|
||||
export interface IntegrationRequest {
|
||||
method: string
|
||||
path: string
|
||||
query?: Record<string, string>
|
||||
headers?: Record<string, string>
|
||||
body?: any
|
||||
}
|
||||
|
||||
/**
|
||||
* HTTP response from integration
|
||||
*/
|
||||
export interface IntegrationResponse {
|
||||
status: number
|
||||
headers: Record<string, string>
|
||||
body: any
|
||||
isSSE?: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration Hub - Zero-Configuration Integration Manager
|
||||
*
|
||||
* Provides instant access to:
|
||||
* - OData API (Excel Power Query, Power BI, Tableau)
|
||||
* - Google Sheets API (two-way sync)
|
||||
* - SSE streaming (real-time dashboards)
|
||||
* - Webhooks (push notifications)
|
||||
*
|
||||
* All integrations work in any environment with zero external dependencies.
|
||||
*/
|
||||
export class IntegrationHub {
|
||||
private integrations: Map<IntegrationType, IntegrationBase> = new Map()
|
||||
private config: Required<IntegrationHubConfig>
|
||||
private _endpoints: Record<IntegrationType, string> = {} as any
|
||||
|
||||
/**
|
||||
* Create and initialize the Integration Hub
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // All integrations, default paths
|
||||
* const hub = await IntegrationHub.create(brain)
|
||||
*
|
||||
* // Custom base path
|
||||
* const hub = await IntegrationHub.create(brain, { basePath: '/api/v1' })
|
||||
*
|
||||
* // Only specific integrations
|
||||
* const hub = await IntegrationHub.create(brain, { enable: ['odata', 'sheets'] })
|
||||
* ```
|
||||
*/
|
||||
static async create(brain: any, config?: IntegrationHubConfig): Promise<IntegrationHub> {
|
||||
const hub = new IntegrationHub(config)
|
||||
await hub.initialize(brain)
|
||||
return hub
|
||||
}
|
||||
|
||||
private constructor(config?: IntegrationHubConfig) {
|
||||
this.config = {
|
||||
basePath: config?.basePath ?? '',
|
||||
enable: config?.enable ?? 'all',
|
||||
config: config?.config ?? {}
|
||||
}
|
||||
}
|
||||
|
||||
private async initialize(brain: any): Promise<void> {
|
||||
const toEnable = this.config.enable === 'all'
|
||||
? (['odata', 'sheets', 'sse', 'webhooks'] as IntegrationType[])
|
||||
: this.config.enable
|
||||
|
||||
// Create context for integration initialization
|
||||
const context = {
|
||||
brain,
|
||||
storage: brain.getStorage?.() || null,
|
||||
config: {},
|
||||
log: (message: string, level?: string) => {
|
||||
// Silent logging - integrations handle their own logging
|
||||
}
|
||||
}
|
||||
|
||||
for (const type of toEnable) {
|
||||
const integration = await this.createIntegration(type)
|
||||
if (integration) {
|
||||
// Initialize the integration with context (BaseAugmentation pattern)
|
||||
await integration.initialize(context)
|
||||
this.integrations.set(type, integration)
|
||||
this._endpoints[type] = this.getBasePath(type)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private async createIntegration(type: IntegrationType): Promise<IntegrationBase | null> {
|
||||
const basePath = this.config.basePath
|
||||
const cfg = this.config.config?.[type]
|
||||
|
||||
switch (type) {
|
||||
case 'odata':
|
||||
return new ODataIntegration({
|
||||
...cfg,
|
||||
basePath: cfg?.basePath ?? `${basePath}/odata`
|
||||
})
|
||||
case 'sheets':
|
||||
return new GoogleSheetsIntegration({
|
||||
...cfg,
|
||||
basePath: cfg?.basePath ?? `${basePath}/sheets`
|
||||
})
|
||||
case 'sse':
|
||||
return new SSEIntegration({
|
||||
...cfg,
|
||||
basePath: cfg?.basePath ?? `${basePath}/events`
|
||||
})
|
||||
case 'webhooks':
|
||||
return new WebhookIntegration(cfg)
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
|
||||
private getBasePath(type: IntegrationType): string {
|
||||
const basePath = this.config.basePath
|
||||
const cfg = this.config.config?.[type] as any
|
||||
|
||||
switch (type) {
|
||||
case 'odata':
|
||||
return cfg?.basePath ?? `${basePath}/odata`
|
||||
case 'sheets':
|
||||
return cfg?.basePath ?? `${basePath}/sheets`
|
||||
case 'sse':
|
||||
return cfg?.basePath ?? `${basePath}/events`
|
||||
case 'webhooks':
|
||||
return `${basePath}/webhooks`
|
||||
default:
|
||||
return ''
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all endpoint paths
|
||||
*/
|
||||
get endpoints(): Record<IntegrationType, string> {
|
||||
return { ...this._endpoints }
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle an HTTP request and route to the appropriate integration
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Express middleware
|
||||
* app.use('/api', async (req, res) => {
|
||||
* const response = await hub.handleRequest({
|
||||
* method: req.method,
|
||||
* path: req.path,
|
||||
* query: req.query,
|
||||
* headers: req.headers,
|
||||
* body: req.body
|
||||
* })
|
||||
*
|
||||
* if (response.isSSE) {
|
||||
* // Handle SSE stream
|
||||
* } else {
|
||||
* res.status(response.status).set(response.headers).json(response.body)
|
||||
* }
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
async handleRequest(request: IntegrationRequest): Promise<IntegrationResponse> {
|
||||
const { path } = request
|
||||
|
||||
// Route to appropriate integration based on path
|
||||
for (const [type, basePath] of Object.entries(this._endpoints)) {
|
||||
if (path.startsWith(basePath) || path === basePath) {
|
||||
const integration = this.integrations.get(type as IntegrationType)
|
||||
if (integration && 'handleRequest' in integration) {
|
||||
const handler = integration as any
|
||||
const relativePath = path.slice(basePath.length) || '/'
|
||||
|
||||
return handler.handleRequest({
|
||||
...request,
|
||||
path: relativePath
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
status: 404,
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: { error: 'Not found', path }
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a specific integration
|
||||
*/
|
||||
get<T extends IntegrationBase>(type: IntegrationType): T | undefined {
|
||||
return this.integrations.get(type) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the OData integration
|
||||
*/
|
||||
get odata(): ODataIntegration | undefined {
|
||||
return this.integrations.get('odata') as ODataIntegration
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Google Sheets integration
|
||||
*/
|
||||
get sheets(): GoogleSheetsIntegration | undefined {
|
||||
return this.integrations.get('sheets') as GoogleSheetsIntegration
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the SSE integration
|
||||
*/
|
||||
get sse(): SSEIntegration | undefined {
|
||||
return this.integrations.get('sse') as SSEIntegration
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the Webhook integration
|
||||
*/
|
||||
get webhooks(): WebhookIntegration | undefined {
|
||||
return this.integrations.get('webhooks') as WebhookIntegration
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an integration is enabled
|
||||
*/
|
||||
has(type: IntegrationType): boolean {
|
||||
return this.integrations.has(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get health status of all integrations
|
||||
*/
|
||||
health(): Record<IntegrationType, IntegrationHealthStatus> {
|
||||
const result: Record<string, IntegrationHealthStatus> = {}
|
||||
|
||||
for (const [type, integration] of this.integrations) {
|
||||
result[type] = integration.health()
|
||||
}
|
||||
|
||||
return result as Record<IntegrationType, IntegrationHealthStatus>
|
||||
}
|
||||
|
||||
/**
|
||||
* Stop all integrations
|
||||
*/
|
||||
async stop(): Promise<void> {
|
||||
for (const integration of this.integrations.values()) {
|
||||
await integration.stop()
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get connection instructions for each tool
|
||||
*/
|
||||
getInstructions(): Record<string, string> {
|
||||
const base = this.config.basePath || 'http://localhost:3000'
|
||||
|
||||
return {
|
||||
excel: `
|
||||
Excel Power Query:
|
||||
1. Data → Get Data → From Other Sources → From OData Feed
|
||||
2. Enter URL: ${base}/odata
|
||||
3. Click OK, then Load
|
||||
`.trim(),
|
||||
|
||||
powerbi: `
|
||||
Power BI:
|
||||
1. Get Data → OData Feed
|
||||
2. Enter URL: ${base}/odata
|
||||
3. Click OK, then Load
|
||||
`.trim(),
|
||||
|
||||
googleSheets: `
|
||||
Google Sheets:
|
||||
1. Install the Brainy add-on from Google Workspace Marketplace
|
||||
2. Open sidebar: Extensions → Brainy → Open
|
||||
3. Connect to: ${base}/sheets
|
||||
4. Use custom functions: =BRAINY_QUERY("type:Person", 100)
|
||||
`.trim(),
|
||||
|
||||
realtime: `
|
||||
Real-time Streaming (SSE):
|
||||
const source = new EventSource('${base}/events')
|
||||
source.onmessage = (event) => console.log(JSON.parse(event.data))
|
||||
`.trim(),
|
||||
|
||||
webhooks: `
|
||||
Webhooks:
|
||||
POST ${base}/webhooks/register
|
||||
{
|
||||
"url": "https://your-server.com/webhook",
|
||||
"events": { "entityTypes": ["noun"], "operations": ["create", "update"] }
|
||||
}
|
||||
`.trim()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an Integration Hub with zero configuration
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const hub = await createIntegrationHub(brain)
|
||||
* console.log(hub.endpoints)
|
||||
* ```
|
||||
*/
|
||||
export async function createIntegrationHub(
|
||||
brain: any,
|
||||
config?: IntegrationHubConfig
|
||||
): Promise<IntegrationHub> {
|
||||
return IntegrationHub.create(brain, config)
|
||||
}
|
||||
279
src/integrations/core/IntegrationLoader.ts
Normal file
279
src/integrations/core/IntegrationLoader.ts
Normal file
|
|
@ -0,0 +1,279 @@
|
|||
/**
|
||||
* Integration Hub - Integration Loader
|
||||
*
|
||||
* Lazy-loads integrations with environment detection.
|
||||
* Zero external dependencies - works everywhere.
|
||||
*/
|
||||
|
||||
import { IntegrationBase } from './IntegrationBase.js'
|
||||
import { IntegrationConfig } from './types.js'
|
||||
|
||||
/**
|
||||
* Supported integration types
|
||||
*/
|
||||
export type IntegrationType =
|
||||
| 'odata' // Excel Power Query, Power BI, Tableau
|
||||
| 'sheets' // Google Sheets two-way sync
|
||||
| 'sse' // Server-Sent Events streaming
|
||||
| 'webhooks' // Push notifications to external URLs
|
||||
|
||||
/**
|
||||
* Runtime environment
|
||||
*/
|
||||
export type RuntimeEnvironment =
|
||||
| 'node'
|
||||
| 'browser'
|
||||
| 'deno'
|
||||
| 'cloudflare'
|
||||
| 'bun'
|
||||
|
||||
/**
|
||||
* Integration metadata
|
||||
*/
|
||||
export interface IntegrationInfo {
|
||||
id: IntegrationType
|
||||
name: string
|
||||
description: string
|
||||
environments: RuntimeEnvironment[]
|
||||
tools: string[] // What tools this works with
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration catalog - all built-in, zero dependencies
|
||||
*/
|
||||
export const INTEGRATION_CATALOG: Record<IntegrationType, IntegrationInfo> = {
|
||||
odata: {
|
||||
id: 'odata',
|
||||
name: 'OData 4.0 API',
|
||||
description: 'REST API for spreadsheets and BI tools',
|
||||
environments: ['node', 'browser', 'deno', 'cloudflare', 'bun'],
|
||||
tools: ['Excel Power Query', 'Power BI', 'Tableau', 'Qlik', 'SAP']
|
||||
},
|
||||
sheets: {
|
||||
id: 'sheets',
|
||||
name: 'Google Sheets',
|
||||
description: 'Two-way sync with Google Sheets',
|
||||
environments: ['node', 'browser', 'deno', 'cloudflare', 'bun'],
|
||||
tools: ['Google Sheets', 'Apps Script']
|
||||
},
|
||||
sse: {
|
||||
id: 'sse',
|
||||
name: 'Real-time Streaming',
|
||||
description: 'Server-Sent Events for live updates',
|
||||
environments: ['node', 'browser', 'deno', 'cloudflare', 'bun'],
|
||||
tools: ['Dashboards', 'Live UIs', 'Monitoring']
|
||||
},
|
||||
webhooks: {
|
||||
id: 'webhooks',
|
||||
name: 'Webhooks',
|
||||
description: 'Push events to external URLs',
|
||||
environments: ['node', 'browser', 'deno', 'cloudflare', 'bun'],
|
||||
tools: ['Zapier', 'IFTTT', 'n8n', 'Custom APIs']
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Detect current runtime environment
|
||||
*/
|
||||
export function detectEnvironment(): RuntimeEnvironment {
|
||||
// Deno
|
||||
if (typeof (globalThis as any).Deno !== 'undefined') {
|
||||
return 'deno'
|
||||
}
|
||||
|
||||
// Bun
|
||||
if (typeof (globalThis as any).Bun !== 'undefined') {
|
||||
return 'bun'
|
||||
}
|
||||
|
||||
// Cloudflare Workers
|
||||
if (
|
||||
typeof (globalThis as any).caches !== 'undefined' &&
|
||||
typeof (globalThis as any).HTMLRewriter !== 'undefined'
|
||||
) {
|
||||
return 'cloudflare'
|
||||
}
|
||||
|
||||
// Node.js
|
||||
if (
|
||||
typeof process !== 'undefined' &&
|
||||
process.versions &&
|
||||
process.versions.node
|
||||
) {
|
||||
return 'node'
|
||||
}
|
||||
|
||||
// Browser
|
||||
if (typeof window !== 'undefined') {
|
||||
return 'browser'
|
||||
}
|
||||
|
||||
return 'node'
|
||||
}
|
||||
|
||||
/**
|
||||
* Integration loader configuration
|
||||
*/
|
||||
export interface IntegrationLoaderConfig {
|
||||
/** Which integrations to load: array of types, 'all', or 'none' */
|
||||
integrations?: IntegrationType[] | 'all' | 'none'
|
||||
|
||||
/** Override configs per integration */
|
||||
config?: Partial<Record<IntegrationType, IntegrationConfig>>
|
||||
|
||||
/** Custom integrations to add */
|
||||
custom?: IntegrationBase[]
|
||||
}
|
||||
|
||||
/**
|
||||
* Lazy-loading integration manager
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* // Load all integrations (recommended)
|
||||
* const loader = new IntegrationLoader({ integrations: 'all' })
|
||||
* const integrations = await loader.load()
|
||||
*
|
||||
* // Load specific integrations
|
||||
* const loader = new IntegrationLoader({
|
||||
* integrations: ['odata', 'sheets']
|
||||
* })
|
||||
* ```
|
||||
*/
|
||||
export class IntegrationLoader {
|
||||
private config: IntegrationLoaderConfig
|
||||
private environment: RuntimeEnvironment
|
||||
private loaded: Map<IntegrationType, IntegrationBase> = new Map()
|
||||
|
||||
constructor(config: IntegrationLoaderConfig = {}) {
|
||||
this.config = {
|
||||
integrations: config.integrations ?? 'none',
|
||||
config: config.config ?? {},
|
||||
custom: config.custom ?? []
|
||||
}
|
||||
this.environment = detectEnvironment()
|
||||
}
|
||||
|
||||
/**
|
||||
* Get current runtime environment
|
||||
*/
|
||||
getEnvironment(): RuntimeEnvironment {
|
||||
return this.environment
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all available integrations
|
||||
*/
|
||||
getAvailable(): IntegrationInfo[] {
|
||||
return Object.values(INTEGRATION_CATALOG).filter((info) =>
|
||||
info.environments.includes(this.environment)
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an integration is available
|
||||
*/
|
||||
isAvailable(type: IntegrationType): boolean {
|
||||
const info = INTEGRATION_CATALOG[type]
|
||||
return info?.environments.includes(this.environment) ?? false
|
||||
}
|
||||
|
||||
/**
|
||||
* Load and instantiate integrations
|
||||
*/
|
||||
async load(): Promise<IntegrationBase[]> {
|
||||
const toLoad = this.resolveIntegrations()
|
||||
const results: IntegrationBase[] = []
|
||||
|
||||
// Load integrations in parallel for speed
|
||||
const loadPromises = toLoad.map(async (type) => {
|
||||
try {
|
||||
const integration = await this.loadOne(type)
|
||||
if (integration) {
|
||||
this.loaded.set(type, integration)
|
||||
return integration
|
||||
}
|
||||
} catch (error: any) {
|
||||
console.warn(`[Brainy] Failed to load ${type}: ${error.message}`)
|
||||
}
|
||||
return null
|
||||
})
|
||||
|
||||
const loadedIntegrations = await Promise.all(loadPromises)
|
||||
results.push(...loadedIntegrations.filter((i): i is IntegrationBase => i !== null))
|
||||
|
||||
// Add custom integrations
|
||||
for (const custom of this.config.custom ?? []) {
|
||||
results.push(custom)
|
||||
}
|
||||
|
||||
return results
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a loaded integration by type
|
||||
*/
|
||||
get<T extends IntegrationBase = IntegrationBase>(type: IntegrationType): T | undefined {
|
||||
return this.loaded.get(type) as T | undefined
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if an integration is loaded
|
||||
*/
|
||||
has(type: IntegrationType): boolean {
|
||||
return this.loaded.has(type)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all loaded integrations
|
||||
*/
|
||||
all(): IntegrationBase[] {
|
||||
return Array.from(this.loaded.values())
|
||||
}
|
||||
|
||||
private resolveIntegrations(): IntegrationType[] {
|
||||
const { integrations } = this.config
|
||||
|
||||
if (integrations === 'none') {
|
||||
return []
|
||||
}
|
||||
|
||||
if (integrations === 'all') {
|
||||
return this.getAvailable().map((info) => info.id)
|
||||
}
|
||||
|
||||
return (integrations || []).filter((type) => this.isAvailable(type))
|
||||
}
|
||||
|
||||
private async loadOne(type: IntegrationType): Promise<IntegrationBase | null> {
|
||||
const cfg = this.config.config?.[type]
|
||||
|
||||
switch (type) {
|
||||
case 'odata': {
|
||||
const { ODataIntegration } = await import('../odata/ODataIntegration.js')
|
||||
return new ODataIntegration(cfg)
|
||||
}
|
||||
case 'sheets': {
|
||||
const { GoogleSheetsIntegration } = await import('../sheets/GoogleSheetsIntegration.js')
|
||||
return new GoogleSheetsIntegration(cfg)
|
||||
}
|
||||
case 'sse': {
|
||||
const { SSEIntegration } = await import('../sse/SSEIntegration.js')
|
||||
return new SSEIntegration(cfg)
|
||||
}
|
||||
case 'webhooks': {
|
||||
const { WebhookIntegration } = await import('../webhooks/WebhookIntegration.js')
|
||||
return new WebhookIntegration(cfg)
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Create an integration loader
|
||||
*/
|
||||
export function createIntegrationLoader(config?: IntegrationLoaderConfig): IntegrationLoader {
|
||||
return new IntegrationLoader(config)
|
||||
}
|
||||
574
src/integrations/core/TabularExporter.ts
Normal file
574
src/integrations/core/TabularExporter.ts
Normal file
|
|
@ -0,0 +1,574 @@
|
|||
/**
|
||||
* Integration Hub - Tabular Exporter
|
||||
*
|
||||
* Converts Brainy entities to tabular formats (rows/columns) for use in
|
||||
* spreadsheets, SQL databases, and BI tools.
|
||||
*/
|
||||
|
||||
import { Entity, Relation } from '../../types/brainy.types.js'
|
||||
import {
|
||||
TabularRow,
|
||||
RelationTabularRow,
|
||||
TabularExporterConfig
|
||||
} from './types.js'
|
||||
|
||||
/**
|
||||
* Converts Brainy entities to tabular formats
|
||||
*
|
||||
* Used by SQL, OData, Google Sheets, and CSV integrations to maintain
|
||||
* consistent entity-to-row mapping across all external tools.
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const exporter = new TabularExporter({
|
||||
* flattenMetadata: true,
|
||||
* includeVectors: false
|
||||
* })
|
||||
*
|
||||
* const rows = exporter.entitiesToRows(entities)
|
||||
* const csv = exporter.toCSV(entities)
|
||||
* const odata = exporter.toOData(entities)
|
||||
* ```
|
||||
*/
|
||||
export class TabularExporter {
|
||||
private config: Required<TabularExporterConfig>
|
||||
|
||||
constructor(config: TabularExporterConfig = {}) {
|
||||
this.config = {
|
||||
flattenMetadata: config.flattenMetadata ?? true,
|
||||
metadataPrefix: config.metadataPrefix ?? 'Metadata_',
|
||||
includeVectors: config.includeVectors ?? false,
|
||||
dateFormat: config.dateFormat ?? 'ISO8601',
|
||||
jsonStringify: config.jsonStringify ?? ['data'],
|
||||
maxFlattenDepth: config.maxFlattenDepth ?? 1, // Flatten one level, stringify deeper
|
||||
excludeColumns: config.excludeColumns ?? []
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert entities to tabular rows
|
||||
*/
|
||||
entitiesToRows(entities: Entity[]): TabularRow[] {
|
||||
return entities.map((entity) => this.entityToRow(entity))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single entity to a tabular row
|
||||
*/
|
||||
entityToRow(entity: Entity): TabularRow {
|
||||
const row: TabularRow = {
|
||||
Id: entity.id,
|
||||
Type: entity.type,
|
||||
CreatedAt: this.formatDate(entity.createdAt),
|
||||
UpdatedAt: entity.updatedAt
|
||||
? this.formatDate(entity.updatedAt)
|
||||
: this.formatDate(entity.createdAt),
|
||||
Confidence: entity.confidence ?? null,
|
||||
Weight: entity.weight ?? null,
|
||||
Service: entity.service ?? null,
|
||||
Data: this.config.jsonStringify.includes('data')
|
||||
? JSON.stringify(entity.data ?? null)
|
||||
: entity.data ?? null
|
||||
}
|
||||
|
||||
// Include vector if configured
|
||||
if (this.config.includeVectors && entity.vector) {
|
||||
row.Vector = JSON.stringify(Array.from(entity.vector))
|
||||
}
|
||||
|
||||
// Flatten metadata
|
||||
if (this.config.flattenMetadata && entity.metadata) {
|
||||
const flatMetadata = this.flattenObject(
|
||||
entity.metadata,
|
||||
this.config.metadataPrefix,
|
||||
this.config.maxFlattenDepth
|
||||
)
|
||||
Object.assign(row, flatMetadata)
|
||||
} else if (entity.metadata) {
|
||||
row.Metadata = JSON.stringify(entity.metadata)
|
||||
}
|
||||
|
||||
// Remove excluded columns
|
||||
for (const col of this.config.excludeColumns) {
|
||||
delete row[col]
|
||||
}
|
||||
|
||||
return row
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relations to tabular rows
|
||||
*/
|
||||
relationsToRows(relations: Relation[]): RelationTabularRow[] {
|
||||
return relations.map((rel) => this.relationToRow(rel))
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert a single relation to a tabular row
|
||||
*/
|
||||
relationToRow(relation: Relation): RelationTabularRow {
|
||||
return {
|
||||
Id: relation.id,
|
||||
FromId: relation.from,
|
||||
ToId: relation.to,
|
||||
Type: relation.type,
|
||||
Weight: relation.weight ?? null,
|
||||
Confidence: relation.confidence ?? null,
|
||||
CreatedAt: this.formatDate(relation.createdAt),
|
||||
UpdatedAt: relation.updatedAt
|
||||
? this.formatDate(relation.updatedAt)
|
||||
: this.formatDate(relation.createdAt),
|
||||
Service: relation.service ?? null,
|
||||
Metadata: relation.metadata ? JSON.stringify(relation.metadata) : null
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert entities to CSV string
|
||||
*/
|
||||
toCSV(entities: Entity[], options?: { delimiter?: string }): string {
|
||||
const delimiter = options?.delimiter ?? ','
|
||||
const rows = this.entitiesToRows(entities)
|
||||
|
||||
if (rows.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
// Get all columns from all rows
|
||||
const columns = this.getAllColumns(rows)
|
||||
|
||||
// Header row
|
||||
const header = columns.map((col) => this.escapeCSV(col)).join(delimiter)
|
||||
|
||||
// Data rows
|
||||
const dataRows = rows.map((row) =>
|
||||
columns
|
||||
.map((col) => {
|
||||
const value = row[col]
|
||||
return this.escapeCSV(
|
||||
value === null || value === undefined ? '' : String(value)
|
||||
)
|
||||
})
|
||||
.join(delimiter)
|
||||
)
|
||||
|
||||
return [header, ...dataRows].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relations to CSV string
|
||||
*/
|
||||
relationsToCSV(
|
||||
relations: Relation[],
|
||||
options?: { delimiter?: string }
|
||||
): string {
|
||||
const delimiter = options?.delimiter ?? ','
|
||||
const rows = this.relationsToRows(relations)
|
||||
|
||||
if (rows.length === 0) {
|
||||
return ''
|
||||
}
|
||||
|
||||
const columns: (keyof RelationTabularRow)[] = [
|
||||
'Id',
|
||||
'FromId',
|
||||
'ToId',
|
||||
'Type',
|
||||
'Weight',
|
||||
'Confidence',
|
||||
'CreatedAt',
|
||||
'UpdatedAt',
|
||||
'Service',
|
||||
'Metadata'
|
||||
]
|
||||
|
||||
const header = columns.join(delimiter)
|
||||
const dataRows = rows.map((row) =>
|
||||
columns
|
||||
.map((col) => {
|
||||
const value = row[col]
|
||||
return this.escapeCSV(
|
||||
value === null || value === undefined ? '' : String(value)
|
||||
)
|
||||
})
|
||||
.join(delimiter)
|
||||
)
|
||||
|
||||
return [header, ...dataRows].join('\n')
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert entities to OData format (JSON with annotations)
|
||||
*/
|
||||
toOData(
|
||||
entities: Entity[],
|
||||
options?: {
|
||||
context?: string
|
||||
count?: number
|
||||
nextLink?: string
|
||||
}
|
||||
): object {
|
||||
const rows = this.entitiesToRows(entities)
|
||||
|
||||
const result: any = {
|
||||
'@odata.context': options?.context ?? '$metadata#Entities'
|
||||
}
|
||||
|
||||
if (options?.count !== undefined) {
|
||||
result['@odata.count'] = options.count
|
||||
}
|
||||
|
||||
result.value = rows.map((row) => this.rowToODataEntity(row))
|
||||
|
||||
if (options?.nextLink) {
|
||||
result['@odata.nextLink'] = options.nextLink
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert relations to OData format
|
||||
*/
|
||||
relationsToOData(
|
||||
relations: Relation[],
|
||||
options?: {
|
||||
context?: string
|
||||
count?: number
|
||||
nextLink?: string
|
||||
}
|
||||
): object {
|
||||
const rows = this.relationsToRows(relations)
|
||||
|
||||
const result: any = {
|
||||
'@odata.context': options?.context ?? '$metadata#Relationships'
|
||||
}
|
||||
|
||||
if (options?.count !== undefined) {
|
||||
result['@odata.count'] = options.count
|
||||
}
|
||||
|
||||
result.value = rows
|
||||
|
||||
if (options?.nextLink) {
|
||||
result['@odata.nextLink'] = options.nextLink
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse CSV string to entity-like objects
|
||||
*/
|
||||
parseCSV(
|
||||
csv: string,
|
||||
options?: { delimiter?: string }
|
||||
): Partial<Entity>[] {
|
||||
const delimiter = options?.delimiter ?? ','
|
||||
const lines = csv.split('\n').filter((line) => line.trim())
|
||||
|
||||
if (lines.length < 2) {
|
||||
return []
|
||||
}
|
||||
|
||||
const headers = this.parseCSVLine(lines[0], delimiter)
|
||||
const entities: Partial<Entity>[] = []
|
||||
|
||||
for (let i = 1; i < lines.length; i++) {
|
||||
const values = this.parseCSVLine(lines[i], delimiter)
|
||||
const row: Record<string, any> = {}
|
||||
|
||||
for (let j = 0; j < headers.length; j++) {
|
||||
row[headers[j]] = values[j] ?? ''
|
||||
}
|
||||
|
||||
entities.push(this.rowToEntity(row))
|
||||
}
|
||||
|
||||
return entities
|
||||
}
|
||||
|
||||
/**
|
||||
* Get schema from entities (column names and types)
|
||||
*/
|
||||
getSchema(entities: Entity[]): Array<{
|
||||
name: string
|
||||
type: 'string' | 'number' | 'boolean' | 'datetime' | 'json'
|
||||
nullable: boolean
|
||||
}> {
|
||||
const rows = this.entitiesToRows(entities.slice(0, 100)) // Sample first 100
|
||||
const columns = this.getAllColumns(rows)
|
||||
const schema: Array<{
|
||||
name: string
|
||||
type: 'string' | 'number' | 'boolean' | 'datetime' | 'json'
|
||||
nullable: boolean
|
||||
}> = []
|
||||
|
||||
for (const col of columns) {
|
||||
let type: 'string' | 'number' | 'boolean' | 'datetime' | 'json' = 'string'
|
||||
let nullable = false
|
||||
|
||||
for (const row of rows) {
|
||||
const value = row[col]
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
nullable = true
|
||||
continue
|
||||
}
|
||||
|
||||
const inferredType = this.inferType(value)
|
||||
if (type === 'string') {
|
||||
type = inferredType
|
||||
} else if (type !== inferredType) {
|
||||
// Mixed types, fall back to string
|
||||
type = 'string'
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
schema.push({ name: col, type, nullable })
|
||||
}
|
||||
|
||||
return schema
|
||||
}
|
||||
|
||||
// Private helper methods
|
||||
|
||||
private formatDate(timestamp: number): string {
|
||||
switch (this.config.dateFormat) {
|
||||
case 'unix':
|
||||
return Math.floor(timestamp / 1000).toString()
|
||||
case 'unix_ms':
|
||||
return timestamp.toString()
|
||||
case 'ISO8601':
|
||||
default:
|
||||
return new Date(timestamp).toISOString()
|
||||
}
|
||||
}
|
||||
|
||||
private flattenObject(
|
||||
obj: Record<string, any>,
|
||||
prefix: string,
|
||||
maxDepth: number,
|
||||
currentDepth = 0
|
||||
): Record<string, any> {
|
||||
const result: Record<string, any> = {}
|
||||
|
||||
for (const [key, value] of Object.entries(obj)) {
|
||||
const newKey = `${prefix}${key}`
|
||||
|
||||
if (value === null || value === undefined) {
|
||||
result[newKey] = null
|
||||
} else if (Array.isArray(value)) {
|
||||
// Arrays are always JSON stringified
|
||||
result[newKey] = JSON.stringify(value)
|
||||
} else if (typeof value === 'object') {
|
||||
// Objects: flatten if under depth limit, otherwise stringify
|
||||
if (currentDepth < maxDepth - 1) {
|
||||
Object.assign(
|
||||
result,
|
||||
this.flattenObject(value, `${newKey}_`, maxDepth, currentDepth + 1)
|
||||
)
|
||||
} else {
|
||||
// Max depth reached - stringify the object
|
||||
result[newKey] = JSON.stringify(value)
|
||||
}
|
||||
} else {
|
||||
// Primitives: use as-is
|
||||
result[newKey] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private getAllColumns(rows: TabularRow[]): string[] {
|
||||
const columnSet = new Set<string>()
|
||||
|
||||
// Standard columns first
|
||||
const standardColumns = [
|
||||
'Id',
|
||||
'Type',
|
||||
'CreatedAt',
|
||||
'UpdatedAt',
|
||||
'Confidence',
|
||||
'Weight',
|
||||
'Service',
|
||||
'Data'
|
||||
]
|
||||
|
||||
for (const col of standardColumns) {
|
||||
columnSet.add(col)
|
||||
}
|
||||
|
||||
// Add all other columns from rows
|
||||
for (const row of rows) {
|
||||
for (const key of Object.keys(row)) {
|
||||
columnSet.add(key)
|
||||
}
|
||||
}
|
||||
|
||||
return Array.from(columnSet)
|
||||
}
|
||||
|
||||
private escapeCSV(value: string): string {
|
||||
// Escape quotes and wrap in quotes if contains special characters
|
||||
if (
|
||||
value.includes(',') ||
|
||||
value.includes('"') ||
|
||||
value.includes('\n') ||
|
||||
value.includes('\r')
|
||||
) {
|
||||
return `"${value.replace(/"/g, '""')}"`
|
||||
}
|
||||
return value
|
||||
}
|
||||
|
||||
private parseCSVLine(line: string, delimiter: string): string[] {
|
||||
const result: string[] = []
|
||||
let current = ''
|
||||
let inQuotes = false
|
||||
|
||||
for (let i = 0; i < line.length; i++) {
|
||||
const char = line[i]
|
||||
|
||||
if (inQuotes) {
|
||||
if (char === '"') {
|
||||
if (line[i + 1] === '"') {
|
||||
current += '"'
|
||||
i++
|
||||
} else {
|
||||
inQuotes = false
|
||||
}
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
} else {
|
||||
if (char === '"') {
|
||||
inQuotes = true
|
||||
} else if (char === delimiter) {
|
||||
result.push(current)
|
||||
current = ''
|
||||
} else {
|
||||
current += char
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
result.push(current)
|
||||
return result
|
||||
}
|
||||
|
||||
private rowToEntity(row: Record<string, any>): Partial<Entity> {
|
||||
const entity: Partial<Entity> = {}
|
||||
|
||||
// Map standard columns
|
||||
if (row.Id) entity.id = row.Id
|
||||
if (row.Type) entity.type = row.Type as any
|
||||
if (row.Service) entity.service = row.Service
|
||||
if (row.Confidence) entity.confidence = parseFloat(row.Confidence)
|
||||
if (row.Weight) entity.weight = parseFloat(row.Weight)
|
||||
|
||||
// Parse timestamps
|
||||
if (row.CreatedAt) {
|
||||
entity.createdAt = this.parseDate(row.CreatedAt)
|
||||
}
|
||||
if (row.UpdatedAt) {
|
||||
entity.updatedAt = this.parseDate(row.UpdatedAt)
|
||||
}
|
||||
|
||||
// Parse data
|
||||
if (row.Data) {
|
||||
try {
|
||||
entity.data = JSON.parse(row.Data)
|
||||
} catch {
|
||||
entity.data = row.Data
|
||||
}
|
||||
}
|
||||
|
||||
// Collect metadata from prefixed columns
|
||||
const metadata: Record<string, any> = {}
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (key.startsWith(this.config.metadataPrefix)) {
|
||||
const metaKey = key.slice(this.config.metadataPrefix.length)
|
||||
try {
|
||||
metadata[metaKey] = JSON.parse(value as string)
|
||||
} catch {
|
||||
metadata[metaKey] = value
|
||||
}
|
||||
}
|
||||
}
|
||||
if (Object.keys(metadata).length > 0) {
|
||||
entity.metadata = metadata
|
||||
}
|
||||
|
||||
return entity
|
||||
}
|
||||
|
||||
private parseDate(value: string): number {
|
||||
// Try parsing as number (unix timestamp)
|
||||
const num = Number(value)
|
||||
if (!isNaN(num)) {
|
||||
// If it looks like seconds (< year 3000 in seconds)
|
||||
if (num < 32503680000) {
|
||||
return num * 1000
|
||||
}
|
||||
return num
|
||||
}
|
||||
|
||||
// Try parsing as ISO date
|
||||
const date = new Date(value)
|
||||
if (!isNaN(date.getTime())) {
|
||||
return date.getTime()
|
||||
}
|
||||
|
||||
return Date.now()
|
||||
}
|
||||
|
||||
private rowToODataEntity(row: TabularRow): object {
|
||||
const result: any = {}
|
||||
|
||||
for (const [key, value] of Object.entries(row)) {
|
||||
if (value === null) {
|
||||
result[key] = null
|
||||
} else if (key === 'CreatedAt' || key === 'UpdatedAt') {
|
||||
// OData datetime format
|
||||
result[key] = value
|
||||
} else {
|
||||
result[key] = value
|
||||
}
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
private inferType(
|
||||
value: any
|
||||
): 'string' | 'number' | 'boolean' | 'datetime' | 'json' {
|
||||
if (typeof value === 'number') {
|
||||
return 'number'
|
||||
}
|
||||
if (typeof value === 'boolean') {
|
||||
return 'boolean'
|
||||
}
|
||||
if (typeof value === 'string') {
|
||||
// Check if it's a date
|
||||
if (/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/.test(value)) {
|
||||
return 'datetime'
|
||||
}
|
||||
// Check if it's JSON
|
||||
if (
|
||||
(value.startsWith('{') && value.endsWith('}')) ||
|
||||
(value.startsWith('[') && value.endsWith(']'))
|
||||
) {
|
||||
try {
|
||||
JSON.parse(value)
|
||||
return 'json'
|
||||
} catch {
|
||||
// Not valid JSON
|
||||
}
|
||||
}
|
||||
}
|
||||
return 'string'
|
||||
}
|
||||
}
|
||||
64
src/integrations/core/index.ts
Normal file
64
src/integrations/core/index.ts
Normal file
|
|
@ -0,0 +1,64 @@
|
|||
/**
|
||||
* Integration Hub - Core Infrastructure
|
||||
*
|
||||
* Shared foundation for all integrations:
|
||||
* - EventBus: Real-time change notifications
|
||||
* - TabularExporter: Entity to rows/columns conversion
|
||||
* - IntegrationBase: Base class for integrations
|
||||
* - IntegrationHub: Zero-config integration manager
|
||||
*/
|
||||
|
||||
// Event system
|
||||
export { EventBus } from './EventBus.js'
|
||||
|
||||
// Tabular export
|
||||
export { TabularExporter } from './TabularExporter.js'
|
||||
|
||||
// Base class
|
||||
export {
|
||||
IntegrationBase,
|
||||
type HTTPIntegration,
|
||||
type StreamingIntegration
|
||||
} from './IntegrationBase.js'
|
||||
|
||||
// Integration loader
|
||||
export {
|
||||
IntegrationLoader,
|
||||
createIntegrationLoader,
|
||||
detectEnvironment,
|
||||
INTEGRATION_CATALOG,
|
||||
type IntegrationType,
|
||||
type RuntimeEnvironment,
|
||||
type IntegrationInfo,
|
||||
type IntegrationLoaderConfig
|
||||
} from './IntegrationLoader.js'
|
||||
|
||||
// Zero-config hub
|
||||
export {
|
||||
IntegrationHub,
|
||||
createIntegrationHub,
|
||||
type IntegrationHubConfig,
|
||||
type IntegrationRequest,
|
||||
type IntegrationResponse
|
||||
} from './IntegrationHub.js'
|
||||
|
||||
// Types
|
||||
export type {
|
||||
// Events
|
||||
BrainyEvent,
|
||||
EventFilter,
|
||||
EventHandler,
|
||||
EventSubscription,
|
||||
// Tabular
|
||||
TabularRow,
|
||||
RelationTabularRow,
|
||||
TabularExporterConfig,
|
||||
// Config
|
||||
IntegrationConfig,
|
||||
IntegrationHealthStatus,
|
||||
// OData
|
||||
ODataQueryOptions,
|
||||
// Webhooks
|
||||
WebhookRegistration,
|
||||
WebhookDeliveryResult
|
||||
} from './types.js'
|
||||
261
src/integrations/core/types.ts
Normal file
261
src/integrations/core/types.ts
Normal file
|
|
@ -0,0 +1,261 @@
|
|||
/**
|
||||
* Integration Hub - Shared Types
|
||||
*
|
||||
* Types for OData, Google Sheets, SSE, and Webhooks integrations.
|
||||
* Zero external dependencies.
|
||||
*/
|
||||
|
||||
import { Entity, Relation } from '../../types/brainy.types.js'
|
||||
import { NounType, VerbType } from '../../types/graphTypes.js'
|
||||
|
||||
// ============================================================================
|
||||
// Events - Real-time change notifications
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Real-time event emitted when Brainy data changes
|
||||
*/
|
||||
export interface BrainyEvent {
|
||||
/** Unique event identifier */
|
||||
id: string
|
||||
|
||||
/** What changed: noun, verb, or VFS */
|
||||
entityType: 'noun' | 'verb' | 'vfs'
|
||||
|
||||
/** What happened */
|
||||
operation: 'create' | 'update' | 'delete'
|
||||
|
||||
/** The entity ID that was affected */
|
||||
entityId: string
|
||||
|
||||
/** Unix timestamp in milliseconds */
|
||||
timestamp: number
|
||||
|
||||
/** Monotonically increasing sequence ID for ordering/resumption */
|
||||
sequenceId: bigint
|
||||
|
||||
/** NounType if entityType is 'noun' */
|
||||
nounType?: NounType
|
||||
|
||||
/** VerbType if entityType is 'verb' */
|
||||
verbType?: VerbType
|
||||
|
||||
/** Service (multi-tenancy) */
|
||||
service?: string
|
||||
|
||||
/** Full entity data (if includeData is enabled) */
|
||||
data?: Entity | Relation
|
||||
}
|
||||
|
||||
/**
|
||||
* Filter for subscribing to events
|
||||
*/
|
||||
export interface EventFilter {
|
||||
/** Filter by entity types */
|
||||
entityTypes?: ('noun' | 'verb' | 'vfs')[]
|
||||
|
||||
/** Filter by operations */
|
||||
operations?: ('create' | 'update' | 'delete')[]
|
||||
|
||||
/** Filter by noun types */
|
||||
nounTypes?: NounType[]
|
||||
|
||||
/** Filter by verb types */
|
||||
verbTypes?: VerbType[]
|
||||
|
||||
/** Filter by service */
|
||||
service?: string
|
||||
|
||||
/** Resume from this sequence ID */
|
||||
since?: bigint
|
||||
}
|
||||
|
||||
/**
|
||||
* Event handler function
|
||||
*/
|
||||
export type EventHandler = (event: BrainyEvent) => void | Promise<void>
|
||||
|
||||
/**
|
||||
* Event subscription handle
|
||||
*/
|
||||
export interface EventSubscription {
|
||||
id: string
|
||||
unsubscribe(): void
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Tabular Export - Entity to rows/columns conversion
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Tabular row representation of an entity
|
||||
*/
|
||||
export interface TabularRow {
|
||||
Id: string
|
||||
Type: string
|
||||
CreatedAt: string
|
||||
UpdatedAt: string
|
||||
Confidence: number | null
|
||||
Weight: number | null
|
||||
Service: string | null
|
||||
Data: string | null
|
||||
/** Flattened metadata columns (Metadata_*) */
|
||||
[key: string]: any
|
||||
}
|
||||
|
||||
/**
|
||||
* Tabular row for relations
|
||||
*/
|
||||
export interface RelationTabularRow {
|
||||
Id: string
|
||||
FromId: string
|
||||
ToId: string
|
||||
Type: string
|
||||
Weight: number | null
|
||||
Confidence: number | null
|
||||
CreatedAt: string
|
||||
UpdatedAt: string
|
||||
Service: string | null
|
||||
Metadata: string | null
|
||||
}
|
||||
|
||||
/**
|
||||
* Configuration for TabularExporter
|
||||
*/
|
||||
export interface TabularExporterConfig {
|
||||
/** Flatten metadata into separate columns (default: true) */
|
||||
flattenMetadata?: boolean
|
||||
|
||||
/** Prefix for metadata columns (default: 'Metadata_') */
|
||||
metadataPrefix?: string
|
||||
|
||||
/** Include vector embeddings (default: false) */
|
||||
includeVectors?: boolean
|
||||
|
||||
/** Date format (default: 'ISO8601') */
|
||||
dateFormat?: 'ISO8601' | 'unix' | 'unix_ms'
|
||||
|
||||
/** Fields to JSON.stringify (default: ['data']) */
|
||||
jsonStringify?: string[]
|
||||
|
||||
/** Max depth for flattening nested objects (default: 2) */
|
||||
maxFlattenDepth?: number
|
||||
|
||||
/** Columns to exclude */
|
||||
excludeColumns?: string[]
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Integration Configuration
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Base configuration for all integrations
|
||||
*/
|
||||
export interface IntegrationConfig {
|
||||
/** Enable/disable the integration */
|
||||
enabled?: boolean
|
||||
|
||||
/** Rate limiting */
|
||||
rateLimit?: {
|
||||
max: number
|
||||
windowMs: number
|
||||
}
|
||||
|
||||
/** Authentication */
|
||||
auth?: {
|
||||
required: boolean
|
||||
apiKeys?: string[]
|
||||
}
|
||||
|
||||
/** CORS settings */
|
||||
cors?: {
|
||||
origin: string | string[]
|
||||
methods?: string[]
|
||||
credentials?: boolean
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Health status for an integration
|
||||
*/
|
||||
export interface IntegrationHealthStatus {
|
||||
name: string
|
||||
status: 'healthy' | 'degraded' | 'unhealthy' | 'stopped'
|
||||
message?: string
|
||||
uptimeMs?: number
|
||||
requestCount?: number
|
||||
errorCount?: number
|
||||
lastError?: string
|
||||
checkedAt: number
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// OData - Excel Power Query, Power BI, Tableau
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* OData query options parsed from URL
|
||||
*/
|
||||
export interface ODataQueryOptions {
|
||||
/** $filter expression */
|
||||
filter?: string
|
||||
|
||||
/** $select columns */
|
||||
select?: string[]
|
||||
|
||||
/** $orderby specification */
|
||||
orderBy?: Array<{ field: string; direction: 'asc' | 'desc' }>
|
||||
|
||||
/** $top (limit) */
|
||||
top?: number
|
||||
|
||||
/** $skip (offset) */
|
||||
skip?: number
|
||||
|
||||
/** $expand relations */
|
||||
expand?: string[]
|
||||
|
||||
/** $count - include total count */
|
||||
count?: boolean
|
||||
|
||||
/** $search - full text search */
|
||||
search?: string
|
||||
}
|
||||
|
||||
// ============================================================================
|
||||
// Webhooks - Push notifications
|
||||
// ============================================================================
|
||||
|
||||
/**
|
||||
* Webhook registration
|
||||
*/
|
||||
export interface WebhookRegistration {
|
||||
id: string
|
||||
url: string
|
||||
events: EventFilter
|
||||
secret?: string
|
||||
active: boolean
|
||||
retryPolicy?: {
|
||||
maxRetries: number
|
||||
backoffMultiplier: number
|
||||
initialDelayMs: number
|
||||
maxDelayMs: number
|
||||
}
|
||||
createdAt: number
|
||||
lastDeliveryAt?: number
|
||||
failureCount?: number
|
||||
}
|
||||
|
||||
/**
|
||||
* Webhook delivery result
|
||||
*/
|
||||
export interface WebhookDeliveryResult {
|
||||
webhookId: string
|
||||
eventId: string
|
||||
success: boolean
|
||||
statusCode?: number
|
||||
error?: string
|
||||
attempts: number
|
||||
timestamp: number
|
||||
}
|
||||
Reference in a new issue