2026-01-20 16:21:11 -08:00
/ * *
* Google Sheets Integration
*
* Provides REST API endpoints optimized for Google Apps Script to enable
* two - way sync between Brainy and Google Sheets .
*
* Features :
* - Custom function support ( = BRAINY_QUERY ( ) , = BRAINY_ADD ( ) )
* - Real - time updates via SSE subscription
* - Batch operations for performance
* - Simple authentication ( API key or Bearer token )
*
* Zero external dependencies - works in all environments .
* /
import { IntegrationBase , HTTPIntegration } from '../core/IntegrationBase.js'
import { IntegrationConfig } from '../core/types.js'
import { Entity , FindParams } from '../../types/brainy.types.js'
import { NounType , VerbType } from '../../types/graphTypes.js'
/ * *
* Google Sheets integration configuration
* /
export interface GoogleSheetsConfig extends IntegrationConfig {
/** Base path for API routes (default: '/sheets') */
basePath? : string
/** Port to listen on (only for standalone) */
port? : number
/** Maximum results per query (default: 1000) */
maxResults? : number
/** Default query limit (default: 100) */
defaultLimit? : number
/** Allow write operations (default: true) */
allowWrite? : boolean
/** CORS origins to allow (default: Google Sheets) */
allowedOrigins? : string [ ]
}
/ * *
* Sheets API request
* /
interface SheetsRequest {
method : string
path : string
query : Record < string , string >
body? : any
headers : Record < string , string >
}
/ * *
* Sheets API response
* /
interface SheetsResponse {
status : number
headers : Record < string , string >
body : any
}
/ * *
* Google Sheets Integration
*
* Provides a simple REST API optimized for Google Apps Script custom functions .
*
* Endpoints :
* - GET / sheets / query - Query entities ( for = BRAINY_QUERY ( ) )
* - GET / sheets / entity / : id - Get single entity ( for = BRAINY_GET ( ) )
* - GET / sheets / similar - Semantic search ( for = BRAINY_SIMILAR ( ) )
* - GET / sheets / relations - Get relationships ( for = BRAINY_RELATIONS ( ) )
* - POST / sheets / add - Add entity ( for sidebar )
* - POST / sheets / batch - Batch operations ( for range sync )
* - GET / sheets / schema - Get type schema ( for sidebar dropdown )
* - GET / sheets / stream - SSE for real - time updates
*
* Response Format ( optimized for Sheets ) :
* ` ` ` json
* {
* "headers" : [ "Id" , "Type" , "Name" , "Email" ] ,
* "rows" : [
* [ "uuid-1" , "person" , "John Doe" , "john@example.com" ] ,
* [ "uuid-2" , "person" , "Jane Doe" , "jane@example.com" ]
* ] ,
* "count" : 2 ,
* "hasMore" : false
* }
* ` ` `
*
* @example
* ` ` ` typescript
refactor: remove augmentation system and semantic type matching
Remove the entire augmentation pipeline infrastructure (52 files,
~15,000 lines) and the semantic type matching system. These were
unused middleware layers adding complexity without value.
What was removed:
- src/augmentations/ directory (all augmentation implementations)
- src/augmentationManager.ts (pipeline orchestrator)
- src/types/augmentations.ts, src/types/pipelineTypes.ts
- src/shared/default-augmentations.ts
- Semantic type suggestion (BrainyTypes.suggestNoun/suggestVerb)
- src/utils/typeMatching/ (embedding-based type matcher)
What was preserved by relocating:
- Import handlers (CSV, PDF, Excel) -> src/importers/handlers/
- NeuralImportAugmentation -> src/cortex/neuralImportAugmentation.ts
- Type matching utilities -> heuristic inference in consumers
What was simplified:
- brainy.ts: operations call storage directly (no execute() wrapper)
- IntegrationBase: standalone class (no BaseAugmentation parent)
- BrainyTypes: validation-only (nouns, verbs, isValid*, get*)
- Pipeline: direct execution (no augmentation interception)
- index.ts: removed TypeSuggestion, suggestType exports
- package.json: removed stale types/augmentations export
Build passes, 1176 tests pass, 0 failures.
2026-02-01 10:48:56 -08:00
* const sheets = new GoogleSheetsIntegration ( {
2026-01-20 16:21:11 -08:00
* basePath : '/sheets' ,
* maxResults : 1000
refactor: remove augmentation system and semantic type matching
Remove the entire augmentation pipeline infrastructure (52 files,
~15,000 lines) and the semantic type matching system. These were
unused middleware layers adding complexity without value.
What was removed:
- src/augmentations/ directory (all augmentation implementations)
- src/augmentationManager.ts (pipeline orchestrator)
- src/types/augmentations.ts, src/types/pipelineTypes.ts
- src/shared/default-augmentations.ts
- Semantic type suggestion (BrainyTypes.suggestNoun/suggestVerb)
- src/utils/typeMatching/ (embedding-based type matcher)
What was preserved by relocating:
- Import handlers (CSV, PDF, Excel) -> src/importers/handlers/
- NeuralImportAugmentation -> src/cortex/neuralImportAugmentation.ts
- Type matching utilities -> heuristic inference in consumers
What was simplified:
- brainy.ts: operations call storage directly (no execute() wrapper)
- IntegrationBase: standalone class (no BaseAugmentation parent)
- BrainyTypes: validation-only (nouns, verbs, isValid*, get*)
- Pipeline: direct execution (no augmentation interception)
- index.ts: removed TypeSuggestion, suggestType exports
- package.json: removed stale types/augmentations export
Build passes, 1176 tests pass, 0 failures.
2026-02-01 10:48:56 -08:00
* } )
* await sheets . initialize ( )
2026-01-20 16:21:11 -08:00
* ` ` `
* /
export class GoogleSheetsIntegration
extends IntegrationBase
implements HTTPIntegration
{
readonly name = 'sheets'
port : number
basePath : string
private sheetsConfig : GoogleSheetsConfig & {
enabled : boolean
basePath : string
port : number
maxResults : number
defaultLimit : number
allowWrite : boolean
allowedOrigins : string [ ]
}
private sseClients : Map < string , ( event : string , data : any ) = > void > =
new Map ( )
constructor ( config? : GoogleSheetsConfig ) {
super ( config )
this . sheetsConfig = {
enabled : config?.enabled ? ? true ,
basePath : config?.basePath ? ? '/sheets' ,
port : config?.port ? ? 0 ,
maxResults : config?.maxResults ? ? 1000 ,
defaultLimit : config?.defaultLimit ? ? 100 ,
allowWrite : config?.allowWrite ? ? true ,
allowedOrigins : config?.allowedOrigins ? ? [
'https://docs.google.com' ,
'https://script.google.com'
] ,
rateLimit : config?.rateLimit ,
auth : config?.auth ,
cors : config?.cors ? ? {
origin : [
'https://docs.google.com' ,
'https://script.google.com' ,
'*'
] ,
methods : [ 'GET' , 'POST' , 'OPTIONS' ] ,
credentials : true
}
}
this . port = this . sheetsConfig . port
this . basePath = this . sheetsConfig . basePath
}
protected async onStart ( ) : Promise < void > {
// Subscribe to changes for real-time sync
this . subscribeToChanges (
{ entityTypes : [ 'noun' , 'verb' ] } ,
( event ) = > {
// Broadcast to all SSE clients
this . broadcastSSE ( 'change' , {
type : event . entityType ,
operation : event.operation ,
entityId : event.entityId ,
timestamp : event.timestamp
} )
}
)
this . log ( 'Google Sheets integration started' )
}
protected async onStop ( ) : Promise < void > {
// Close all SSE connections
this . sseClients . clear ( )
this . log ( 'Google Sheets integration stopped' )
}
/ * *
* Handle a Sheets API request
* /
async handleRequest ( request : SheetsRequest ) : Promise < SheetsResponse > {
this . recordRequest ( )
try {
const { method , path } = request
const relativePath = path . startsWith ( this . basePath )
? path . slice ( this . basePath . length )
: path
// CORS preflight
if ( method === 'OPTIONS' ) {
return this . corsResponse ( )
}
// Route the request
if ( method === 'GET' ) {
if ( relativePath === '/query' || relativePath === '' ) {
return this . handleQuery ( request )
}
if ( relativePath . startsWith ( '/entity/' ) ) {
const id = relativePath . slice ( '/entity/' . length )
return this . handleGetEntity ( id )
}
if ( relativePath === '/similar' ) {
return this . handleSimilar ( request )
}
if ( relativePath === '/relations' ) {
return this . handleRelations ( request )
}
if ( relativePath === '/schema' ) {
return this . handleSchema ( )
}
if ( relativePath === '/stream' ) {
return this . handleStream ( request )
}
if ( relativePath === '/health' ) {
return this . handleHealth ( )
}
}
if ( method === 'POST' && this . sheetsConfig . allowWrite ) {
if ( relativePath === '/add' ) {
return this . handleAdd ( request )
}
if ( relativePath === '/update' ) {
return this . handleUpdate ( request )
}
if ( relativePath === '/delete' ) {
return this . handleDelete ( request )
}
if ( relativePath === '/batch' ) {
return this . handleBatch ( request )
}
if ( relativePath === '/relate' ) {
return this . handleRelate ( request )
}
}
return this . errorResponse ( 404 , 'Not Found' )
} catch ( error : any ) {
this . recordError ( error )
return this . errorResponse ( 500 , error . message )
}
}
/ * *
* Get registered routes
* /
getRoutes ( ) : Array < { method : string ; path : string ; description : string } > {
const routes = [
{ method : 'GET' , path : ` ${ this . basePath } /query ` , description : 'Query entities' } ,
{ method : 'GET' , path : ` ${ this . basePath } /entity/:id ` , description : 'Get entity by ID' } ,
{ method : 'GET' , path : ` ${ this . basePath } /similar ` , description : 'Semantic search' } ,
{ method : 'GET' , path : ` ${ this . basePath } /relations ` , description : 'Get relationships' } ,
{ method : 'GET' , path : ` ${ this . basePath } /schema ` , description : 'Get schema' } ,
{ method : 'GET' , path : ` ${ this . basePath } /stream ` , description : 'SSE stream' } ,
{ method : 'GET' , path : ` ${ this . basePath } /health ` , description : 'Health check' }
]
if ( this . sheetsConfig . allowWrite ) {
routes . push (
{ method : 'POST' , path : ` ${ this . basePath } /add ` , description : 'Add entity' } ,
{ method : 'POST' , path : ` ${ this . basePath } /update ` , description : 'Update entity' } ,
{ method : 'POST' , path : ` ${ this . basePath } /delete ` , description : 'Delete entity' } ,
{ method : 'POST' , path : ` ${ this . basePath } /batch ` , description : 'Batch operations' } ,
{ method : 'POST' , path : ` ${ this . basePath } /relate ` , description : 'Create relationship' }
)
}
return routes
}
/ * *
* Register an SSE client
* /
registerSSEClient (
clientId : string ,
sendFn : ( event : string , data : any ) = > void
) : ( ) = > void {
this . sseClients . set ( clientId , sendFn )
return ( ) = > this . sseClients . delete ( clientId )
}
/ * *
* Get manifest
* /
refactor: remove augmentation system and semantic type matching
Remove the entire augmentation pipeline infrastructure (52 files,
~15,000 lines) and the semantic type matching system. These were
unused middleware layers adding complexity without value.
What was removed:
- src/augmentations/ directory (all augmentation implementations)
- src/augmentationManager.ts (pipeline orchestrator)
- src/types/augmentations.ts, src/types/pipelineTypes.ts
- src/shared/default-augmentations.ts
- Semantic type suggestion (BrainyTypes.suggestNoun/suggestVerb)
- src/utils/typeMatching/ (embedding-based type matcher)
What was preserved by relocating:
- Import handlers (CSV, PDF, Excel) -> src/importers/handlers/
- NeuralImportAugmentation -> src/cortex/neuralImportAugmentation.ts
- Type matching utilities -> heuristic inference in consumers
What was simplified:
- brainy.ts: operations call storage directly (no execute() wrapper)
- IntegrationBase: standalone class (no BaseAugmentation parent)
- BrainyTypes: validation-only (nouns, verbs, isValid*, get*)
- Pipeline: direct execution (no augmentation interception)
- index.ts: removed TypeSuggestion, suggestType exports
- package.json: removed stale types/augmentations export
Build passes, 1176 tests pass, 0 failures.
2026-02-01 10:48:56 -08:00
getManifest ( ) : Record < string , any > {
2026-01-20 16:21:11 -08:00
return {
id : 'sheets' ,
name : 'Google Sheets Integration' ,
version : '1.0.0' ,
description : 'Two-way sync between Brainy and Google Sheets' ,
longDescription :
'Enables real-time bidirectional synchronization with Google Sheets. Use custom functions like =BRAINY_QUERY() directly in cells, or the sidebar for browsing and editing.' ,
category : 'integration' ,
status : 'stable' ,
configSchema : {
type : 'object' ,
properties : {
enabled : { type : 'boolean' , default : true } ,
basePath : { type : 'string' , default : '/sheets' } ,
maxResults : { type : 'number' , default : 1000 } ,
defaultLimit : { type : 'number' , default : 100 } ,
allowWrite : { type : 'boolean' , default : true }
}
} ,
configDefaults : {
enabled : true ,
basePath : '/sheets' ,
maxResults : 1000 ,
defaultLimit : 100 ,
allowWrite : true
} ,
features : [
'Custom functions (=BRAINY_QUERY)' ,
'Real-time sync via SSE' ,
'Batch operations' ,
'Semantic search support' ,
'Type schema discovery'
] ,
keywords : [ 'google-sheets' , 'spreadsheet' , 'sync' , 'real-time' ]
}
}
// Route handlers
private async handleQuery ( request : SheetsRequest ) : Promise < SheetsResponse > {
const { query } = request
// Build find params
const findParams : FindParams = {
limit : Math.min (
parseInt ( query . limit ) || this . sheetsConfig . defaultLimit ,
this . sheetsConfig . maxResults
)
}
// Query string (semantic search)
if ( query . q ) {
findParams . query = query . q
}
// Type filter
if ( query . type ) {
const types = query . type . split ( ',' ) as NounType [ ]
findParams . type = types . length === 1 ? types [ 0 ] : types
}
// Offset pagination
if ( query . offset ) {
findParams . offset = parseInt ( query . offset )
}
// Sort
if ( query . orderBy ) {
findParams . orderBy = query . orderBy
findParams . order = ( query . order as 'asc' | 'desc' ) || 'desc'
}
// Execute query
const entities = await this . queryEntities ( findParams )
// Convert to sheets format
return this . entitiesToSheetsResponse ( entities )
}
private async handleGetEntity ( id : string ) : Promise < SheetsResponse > {
const entity = await this . getEntity ( id )
if ( ! entity ) {
return this . errorResponse ( 404 , 'Entity not found' )
}
return this . entitiesToSheetsResponse ( [ entity ] )
}
private async handleSimilar ( request : SheetsRequest ) : Promise < SheetsResponse > {
if ( ! this . context ) {
return this . errorResponse ( 500 , 'Not initialized' )
}
const { query } = request
if ( ! query . q && ! query . to ) {
return this . errorResponse ( 400 , 'Missing q or to parameter' )
}
const limit = Math . min (
parseInt ( query . limit ) || 10 ,
this . sheetsConfig . maxResults
)
// Semantic similarity search
let results : any [ ]
if ( query . to ) {
// Similar to entity
results = await this . context . brain . similar ( {
to : query.to ,
limit ,
threshold : query.threshold ? parseFloat ( query . threshold ) : undefined
} )
} else {
// Similar to query text
results = await this . context . brain . find ( {
query : query.q ,
limit
} )
}
const entities = results . map ( ( r : any ) = > r . entity || r )
return this . entitiesToSheetsResponse ( entities )
}
private async handleRelations (
request : SheetsRequest
) : Promise < SheetsResponse > {
const { query } = request
const params : any = {
limit : Math.min (
parseInt ( query . limit ) || 100 ,
this . sheetsConfig . maxResults
)
}
if ( query . from ) params . from = query . from
if ( query . to ) params . to = query . to
if ( query . type ) params . type = query . type as VerbType
const relations = await this . queryRelations ( params )
// Convert to sheets format
const headers = [
'Id' ,
'FromId' ,
'ToId' ,
'Type' ,
'Weight' ,
'Confidence' ,
'CreatedAt'
]
const rows = relations . map ( ( r ) = > [
r . id ,
r . from ,
r . to ,
r . type ,
r . weight ? ? 1 ,
r . confidence ? ? 1 ,
new Date ( r . createdAt ) . toISOString ( )
] )
return this . jsonResponse ( {
headers ,
rows ,
count : rows.length ,
hasMore : false
} )
}
private handleSchema ( ) : SheetsResponse {
return this . jsonResponse ( {
nounTypes : Object.values ( NounType ) ,
verbTypes : Object.values ( VerbType ) ,
entityFields : [ 'id' , 'type' , 'data' , 'metadata' , 'confidence' , 'weight' , 'service' , 'createdAt' , 'updatedAt' ] ,
commonMetadataFields : [ 'name' , 'title' , 'description' , 'email' , 'url' , 'tags' , 'category' , 'status' , 'priority' ]
} )
}
private handleStream ( _request : SheetsRequest ) : SheetsResponse {
// This returns headers for SSE - actual streaming is handled by the HTTP server
return {
status : 200 ,
headers : {
'Content-Type' : 'text/event-stream' ,
'Cache-Control' : 'no-cache' ,
Connection : 'keep-alive' ,
'Access-Control-Allow-Origin' : '*'
} ,
body : 'sse' // Signal to HTTP server to handle as SSE
}
}
private handleHealth ( ) : SheetsResponse {
return this . jsonResponse ( {
status : 'ok' ,
integration : 'sheets' ,
uptime : this.startedAt ? Date . now ( ) - this . startedAt : 0 ,
requests : this.requestCount
} )
}
private async handleAdd ( request : SheetsRequest ) : Promise < SheetsResponse > {
if ( ! this . context ) {
return this . errorResponse ( 500 , 'Not initialized' )
}
const { body } = request
if ( ! body || ! body . type ) {
return this . errorResponse ( 400 , 'Missing required field: type' )
}
const entity = await this . context . brain . add ( {
type : body . type as NounType ,
data : body.data ,
metadata : body.metadata ,
confidence : body.confidence ,
weight : body.weight ,
service : body.service
} )
return this . entitiesToSheetsResponse ( [ entity ] )
}
private async handleUpdate ( request : SheetsRequest ) : Promise < SheetsResponse > {
if ( ! this . context ) {
return this . errorResponse ( 500 , 'Not initialized' )
}
const { body } = request
if ( ! body || ! body . id ) {
return this . errorResponse ( 400 , 'Missing required field: id' )
}
await this . context . brain . update ( {
id : body.id ,
data : body.data ,
metadata : body.metadata ,
type : body . type as NounType ,
confidence : body.confidence ,
weight : body.weight ,
merge : body.merge ? ? true
} )
const updated = await this . getEntity ( body . id )
return this . entitiesToSheetsResponse ( updated ? [ updated ] : [ ] )
}
private async handleDelete ( request : SheetsRequest ) : Promise < SheetsResponse > {
if ( ! this . context ) {
return this . errorResponse ( 500 , 'Not initialized' )
}
const { body } = request
if ( ! body || ! body . id ) {
return this . errorResponse ( 400 , 'Missing required field: id' )
}
await this . context . brain . delete ( body . id )
return this . jsonResponse ( { success : true , deleted : body.id } )
}
private async handleBatch ( request : SheetsRequest ) : Promise < SheetsResponse > {
if ( ! this . context ) {
return this . errorResponse ( 500 , 'Not initialized' )
}
const { body } = request
if ( ! body || ! Array . isArray ( body . operations ) ) {
return this . errorResponse ( 400 , 'Missing operations array' )
}
const results : any [ ] = [ ]
for ( const op of body . operations ) {
try {
switch ( op . action ) {
case 'add' :
const added = await this . context . brain . add ( {
type : op . type as NounType ,
data : op.data ,
metadata : op.metadata
} )
results . push ( { success : true , id : added.id , action : 'add' } )
break
case 'update' :
await this . context . brain . update ( {
id : op.id ,
data : op.data ,
metadata : op.metadata ,
merge : true
} )
results . push ( { success : true , id : op.id , action : 'update' } )
break
case 'delete' :
await this . context . brain . delete ( op . id )
results . push ( { success : true , id : op.id , action : 'delete' } )
break
default :
results . push ( {
success : false ,
error : ` Unknown action: ${ op . action } `
} )
}
} catch ( error : any ) {
results . push ( {
success : false ,
id : op.id ,
action : op.action ,
error : error.message
} )
}
}
return this . jsonResponse ( {
results ,
total : body.operations.length ,
successful : results.filter ( ( r ) = > r . success ) . length ,
failed : results.filter ( ( r ) = > ! r . success ) . length
} )
}
private async handleRelate ( request : SheetsRequest ) : Promise < SheetsResponse > {
if ( ! this . context ) {
return this . errorResponse ( 500 , 'Not initialized' )
}
const { body } = request
if ( ! body || ! body . from || ! body . to || ! body . type ) {
return this . errorResponse ( 400 , 'Missing required fields: from, to, type' )
}
const relation = await this . context . brain . relate ( {
from : body . from ,
to : body.to ,
type : body . type as VerbType ,
weight : body.weight ,
metadata : body.metadata
} )
return this . jsonResponse ( {
success : true ,
relation : {
id : relation.id ,
from : relation . from ,
to : relation.to ,
type : relation . type
}
} )
}
// Helpers
private entitiesToSheetsResponse ( entities : Entity [ ] ) : SheetsResponse {
if ( entities . length === 0 ) {
return this . jsonResponse ( {
headers : [ 'Id' , 'Type' , 'CreatedAt' ] ,
rows : [ ] ,
count : 0 ,
hasMore : false
} )
}
// Collect all unique metadata keys
const metadataKeys = new Set < string > ( )
for ( const entity of entities ) {
if ( entity . metadata ) {
for ( const key of Object . keys ( entity . metadata ) ) {
metadataKeys . add ( key )
}
}
}
// Build headers
const baseHeaders = [ 'Id' , 'Type' , 'Confidence' , 'Weight' , 'CreatedAt' ]
const metaHeaders = Array . from ( metadataKeys ) . sort ( )
const headers = [ . . . baseHeaders , . . . metaHeaders , 'Data' ]
// Build rows
const rows = entities . map ( ( entity ) = > {
const baseValues = [
entity . id ,
entity . type ,
entity . confidence ? ? '' ,
entity . weight ? ? '' ,
new Date ( entity . createdAt ) . toISOString ( )
]
const metaValues = metaHeaders . map ( ( key ) = > {
const val = entity . metadata ? . [ key ]
if ( val === undefined || val === null ) return ''
if ( typeof val === 'object' ) return JSON . stringify ( val )
return val
} )
const dataValue = entity . data ? JSON . stringify ( entity . data ) : ''
return [ . . . baseValues , . . . metaValues , dataValue ]
} )
return this . jsonResponse ( {
headers ,
rows ,
count : rows.length ,
hasMore : false
} )
}
private broadcastSSE ( event : string , data : any ) : void {
for ( const [ _ , sendFn ] of this . sseClients ) {
try {
sendFn ( event , data )
} catch {
// Client disconnected
}
}
}
private jsonResponse ( data : any ) : SheetsResponse {
return {
status : 200 ,
headers : {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'GET, POST, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type, Authorization'
} ,
body : JSON.stringify ( data )
}
}
private errorResponse ( status : number , message : string ) : SheetsResponse {
return {
status ,
headers : {
'Content-Type' : 'application/json' ,
'Access-Control-Allow-Origin' : '*'
} ,
body : JSON.stringify ( { error : message } )
}
}
private corsResponse ( ) : SheetsResponse {
return {
status : 204 ,
headers : {
'Access-Control-Allow-Origin' : '*' ,
'Access-Control-Allow-Methods' : 'GET, POST, OPTIONS' ,
'Access-Control-Allow-Headers' : 'Content-Type, Authorization' ,
'Access-Control-Max-Age' : '86400'
} ,
body : null
}
}
}
/ * *
* Package export for @soulcraft / brainy - sheets
* /
export const integration = {
name : 'sheets' ,
version : '1.0.0' ,
description : 'Google Sheets two-way sync with real-time updates' ,
environments : [ 'node' , 'browser' , 'deno' , 'cloudflare' , 'bun' ] ,
create : ( brain : any , config? : GoogleSheetsConfig ) = >
new GoogleSheetsIntegration ( config ) ,
defaultConfig : {
basePath : '/sheets' ,
maxResults : 1000 ,
defaultLimit : 100 ,
allowWrite : true
}
}