2025-07-22 10:24:01 -07:00
#!/usr/bin/env node
#!/usr/bin/env node
import express from 'express' ;
import cors from 'cors' ;
import helmet from 'helmet' ;
import compression from 'compression' ;
import rateLimit from 'express-rate-limit' ;
import { body , param , query , validationResult } from 'express-validator' ;
import { BrainyData , cosineDistance , createStorage } from '@soulcraft/brainy' ;
import { fileURLToPath } from 'url' ;
import { dirname , join } from 'path' ;
const _ _filename = fileURLToPath ( import . meta . url ) ;
const _ _dirname = dirname ( _ _filename ) ;
// Configuration
const PORT = process . env . PORT || 3000 ;
const HOST = process . env . HOST || '0.0.0.0' ;
const DATA _PATH = process . env . BRAINY _DATA _PATH || join ( _ _dirname , '..' , 'data' ) ;
const CORS _ORIGIN = process . env . CORS _ORIGIN || '*' ;
const RATE _LIMIT _WINDOW = parseInt ( process . env . RATE _LIMIT _WINDOW || '900000' ) ; // 15 minutes
const RATE _LIMIT _MAX = parseInt ( process . env . RATE _LIMIT _MAX || '100' ) ; // 100 requests per window
// Cloud Storage Configuration
// The createStorage function will automatically detect and use these environment variables:
// AWS S3: S3_BUCKET_NAME, S3_ACCESS_KEY_ID (or AWS_ACCESS_KEY_ID), S3_SECRET_ACCESS_KEY (or AWS_SECRET_ACCESS_KEY), S3_REGION (or AWS_REGION)
// Cloudflare R2: R2_BUCKET_NAME, R2_ACCOUNT_ID, R2_ACCESS_KEY_ID, R2_SECRET_ACCESS_KEY
// Google Cloud Storage: GCS_BUCKET_NAME, GCS_ACCESS_KEY_ID, GCS_SECRET_ACCESS_KEY, GCS_ENDPOINT
// Force local storage: FORCE_LOCAL_STORAGE=true
// Initialize Express app
const app = express ( ) ;
// Security middleware
app . use ( helmet ( {
contentSecurityPolicy : {
directives : {
defaultSrc : [ "'self'" ] ,
styleSrc : [ "'self'" , "'unsafe-inline'" ] ,
scriptSrc : [ "'self'" ] ,
imgSrc : [ "'self'" , "data:" , "https:" ] ,
} ,
} ,
} ) ) ;
app . use ( cors ( {
origin : CORS _ORIGIN ,
methods : [ 'GET' , 'POST' ] ,
allowedHeaders : [ 'Content-Type' , 'Authorization' ] ,
} ) ) ;
app . use ( compression ( ) ) ;
app . use ( express . json ( { limit : '10mb' } ) ) ;
// Rate limiting
const limiter = rateLimit ( {
windowMs : RATE _LIMIT _WINDOW ,
max : RATE _LIMIT _MAX ,
message : {
error : 'Too many requests from this IP, please try again later.' ,
retryAfter : Math . ceil ( RATE _LIMIT _WINDOW / 1000 )
} ,
standardHeaders : true ,
legacyHeaders : false ,
} ) ;
app . use ( '/api/' , limiter ) ;
// Global BrainyData instance
let brainyInstance = null ;
// Initialize BrainyData instance
async function initializeBrainy ( ) {
if ( brainyInstance ) {
return brainyInstance ;
}
try {
console . log ( 'Initializing Brainy database...' ) ;
// Create storage adapter with cloud support
const storageOptions = {
requestPersistentStorage : true
} ;
feat(core, tests): add standalone getStatistics function and improve storage configuration
- **Core**: Introduced a new `getStatistics` utility function in `statistics.ts` for fetching database statistics at the root level of the library. Enhanced `BrainyData` methods to ensure metadata includes `id` field and refined statistics calculations, excluding verbs from the noun count.
- **Tests**: Added comprehensive test coverage in `statistics.test.ts` for the new utility function, validating proper error handling, statistics accuracy, and consistent results between instance methods and standalone function.
- **Storage Config**: Enabled dynamic support for AWS S3, Cloudflare R2, and Google Cloud Storage in web service configuration, utilizing environment variables for adapter setup. Addressed a race condition in `FileSystemStorage` initialization by deferring path module imports.
**Purpose**: Enhance database analytics by introducing a reusable `getStatistics` function, improve flexibility in storage configuration, and ensure robust testing for reliability and accuracy.
2025-07-23 16:26:59 -07:00
// Add AWS S3 configuration if environment variables are present
if ( process . env . S3 _BUCKET _NAME ) {
storageOptions . s3Storage = {
bucketName : process . env . S3 _BUCKET _NAME ,
region : process . env . S3 _REGION || process . env . AWS _REGION || 'us-east-1' ,
accessKeyId : process . env . S3 _ACCESS _KEY _ID || process . env . AWS _ACCESS _KEY _ID ,
secretAccessKey : process . env . S3 _SECRET _ACCESS _KEY || process . env . AWS _SECRET _ACCESS _KEY ,
sessionToken : process . env . S3 _SESSION _TOKEN || process . env . AWS _SESSION _TOKEN
} ;
}
// Add Cloudflare R2 configuration if environment variables are present
if ( process . env . R2 _BUCKET _NAME ) {
storageOptions . r2Storage = {
bucketName : process . env . R2 _BUCKET _NAME ,
accountId : process . env . R2 _ACCOUNT _ID ,
accessKeyId : process . env . R2 _ACCESS _KEY _ID ,
secretAccessKey : process . env . R2 _SECRET _ACCESS _KEY
} ;
}
// Add Google Cloud Storage configuration if environment variables are present
if ( process . env . GCS _BUCKET _NAME ) {
storageOptions . gcsStorage = {
bucketName : process . env . GCS _BUCKET _NAME ,
region : process . env . GCS _REGION ,
endpoint : process . env . GCS _ENDPOINT ,
accessKeyId : process . env . GCS _ACCESS _KEY _ID ,
secretAccessKey : process . env . GCS _SECRET _ACCESS _KEY
} ;
}
2025-07-22 10:24:01 -07:00
// Check if local storage is forced
if ( process . env . FORCE _LOCAL _STORAGE === 'true' ) {
console . log ( 'Forcing local filesystem storage (FORCE_LOCAL_STORAGE=true)' ) ;
storageOptions . forceFileSystemStorage = true ;
// Set the data path for local storage
if ( DATA _PATH ) {
feat(core, tests): add standalone getStatistics function and improve storage configuration
- **Core**: Introduced a new `getStatistics` utility function in `statistics.ts` for fetching database statistics at the root level of the library. Enhanced `BrainyData` methods to ensure metadata includes `id` field and refined statistics calculations, excluding verbs from the noun count.
- **Tests**: Added comprehensive test coverage in `statistics.test.ts` for the new utility function, validating proper error handling, statistics accuracy, and consistent results between instance methods and standalone function.
- **Storage Config**: Enabled dynamic support for AWS S3, Cloudflare R2, and Google Cloud Storage in web service configuration, utilizing environment variables for adapter setup. Addressed a race condition in `FileSystemStorage` initialization by deferring path module imports.
**Purpose**: Enhance database analytics by introducing a reusable `getStatistics` function, improve flexibility in storage configuration, and ensure robust testing for reliability and accuracy.
2025-07-23 16:26:59 -07:00
// We'll need to import FileSystemStorage and path for forced local storage
2025-07-22 10:24:01 -07:00
const { FileSystemStorage } = await import ( '@soulcraft/brainy' ) ;
feat(core, tests): add standalone getStatistics function and improve storage configuration
- **Core**: Introduced a new `getStatistics` utility function in `statistics.ts` for fetching database statistics at the root level of the library. Enhanced `BrainyData` methods to ensure metadata includes `id` field and refined statistics calculations, excluding verbs from the noun count.
- **Tests**: Added comprehensive test coverage in `statistics.test.ts` for the new utility function, validating proper error handling, statistics accuracy, and consistent results between instance methods and standalone function.
- **Storage Config**: Enabled dynamic support for AWS S3, Cloudflare R2, and Google Cloud Storage in web service configuration, utilizing environment variables for adapter setup. Addressed a race condition in `FileSystemStorage` initialization by deferring path module imports.
**Purpose**: Enhance database analytics by introducing a reusable `getStatistics` function, improve flexibility in storage configuration, and ensure robust testing for reliability and accuracy.
2025-07-23 16:26:59 -07:00
const path = await import ( 'path' ) ;
// Create storage with explicit path handling to avoid race condition
const nounsDir = path . join ( DATA _PATH , 'nouns' ) ;
const verbsDir = path . join ( DATA _PATH , 'verbs' ) ;
const metadataDir = path . join ( DATA _PATH , 'metadata' ) ;
const indexDir = path . join ( DATA _PATH , 'index' ) ;
// Create a storage instance with pre-computed paths
2025-07-22 10:24:01 -07:00
const storage = new FileSystemStorage ( DATA _PATH ) ;
feat(core, tests): add standalone getStatistics function and improve storage configuration
- **Core**: Introduced a new `getStatistics` utility function in `statistics.ts` for fetching database statistics at the root level of the library. Enhanced `BrainyData` methods to ensure metadata includes `id` field and refined statistics calculations, excluding verbs from the noun count.
- **Tests**: Added comprehensive test coverage in `statistics.test.ts` for the new utility function, validating proper error handling, statistics accuracy, and consistent results between instance methods and standalone function.
- **Storage Config**: Enabled dynamic support for AWS S3, Cloudflare R2, and Google Cloud Storage in web service configuration, utilizing environment variables for adapter setup. Addressed a race condition in `FileSystemStorage` initialization by deferring path module imports.
**Purpose**: Enhance database analytics by introducing a reusable `getStatistics` function, improve flexibility in storage configuration, and ensure robust testing for reliability and accuracy.
2025-07-23 16:26:59 -07:00
// Initialize the storage adapter before using it
await storage . init ( ) ;
2025-07-22 10:24:01 -07:00
brainyInstance = new BrainyData ( {
storageAdapter : storage ,
distanceFunction : cosineDistance
} ) ;
}
}
// If not forcing local storage or no specific data path, use createStorage
if ( ! brainyInstance ) {
const storage = await createStorage ( storageOptions ) ;
brainyInstance = new BrainyData ( {
storageAdapter : storage ,
distanceFunction : cosineDistance
} ) ;
}
await brainyInstance . init ( ) ;
// Force read-only mode for security
brainyInstance . setReadOnly ( true ) ;
// Log storage information
console . log ( ` Brainy database initialized in read-only mode ` ) ;
console . log ( ` Database size: ${ brainyInstance . size ( ) } items ` ) ;
return brainyInstance ;
}
catch ( error ) {
console . error ( 'Failed to initialize Brainy database:' , error ) ;
throw error ;
}
}
// Error handler middleware
const handleValidationErrors = ( req , res , next ) => {
const errors = validationResult ( req ) ;
if ( ! errors . isEmpty ( ) ) {
return res . status ( 400 ) . json ( {
error : 'Validation failed' ,
details : errors . array ( )
} ) ;
}
next ( ) ;
} ;
// Health check endpoint
app . get ( '/health' , ( req , res ) => {
res . json ( {
status : 'healthy' ,
timestamp : new Date ( ) . toISOString ( ) ,
service : 'brainy-web-service' ,
version : '0.12.0'
} ) ;
} ) ;
// API Documentation endpoint
app . get ( '/api' , ( req , res ) => {
res . json ( {
service : 'Brainy Web Service' ,
version : '0.12.0' ,
description : 'Read-only search service for Brainy vector graph database' ,
endpoints : {
'GET /health' : 'Health check' ,
'GET /api' : 'API documentation' ,
'GET /api/status' : 'Database status' ,
'POST /api/search' : 'Search for similar vectors' ,
'POST /api/search/text' : 'Search using text query' ,
'GET /api/item/:id' : 'Get item by ID' ,
'GET /api/items' : 'Get all items (paginated)' ,
'POST /api/similar/:id' : 'Find similar items to given ID'
} ,
security : {
readOnly : true ,
rateLimit : {
windowMs : RATE _LIMIT _WINDOW ,
maxRequests : RATE _LIMIT _MAX
}
}
} ) ;
} ) ;
// Database status endpoint
app . get ( '/api/status' , async ( req , res ) => {
try {
const brainy = await initializeBrainy ( ) ;
const status = await brainy . status ( ) ;
res . json ( {
... status ,
readOnly : brainy . isReadOnly ( ) ,
timestamp : new Date ( ) . toISOString ( )
} ) ;
}
catch ( error ) {
console . error ( 'Status check failed:' , error ) ;
res . status ( 500 ) . json ( {
error : 'Failed to get database status' ,
message : error . message
} ) ;
}
} ) ;
// Search endpoint - vector search
app . post ( '/api/search' , [
body ( 'vector' ) . isArray ( ) . withMessage ( 'Vector must be an array' ) ,
body ( 'vector.*' ) . isNumeric ( ) . withMessage ( 'Vector elements must be numbers' ) ,
body ( 'k' ) . optional ( ) . isInt ( { min : 1 , max : 100 } ) . withMessage ( 'k must be between 1 and 100' ) ,
body ( 'nounTypes' ) . optional ( ) . isArray ( ) . withMessage ( 'nounTypes must be an array' ) ,
body ( 'includeVerbs' ) . optional ( ) . isBoolean ( ) . withMessage ( 'includeVerbs must be boolean' ) ,
handleValidationErrors
] , async ( req , res ) => {
try {
const brainy = await initializeBrainy ( ) ;
const { vector , k = 10 , nounTypes , includeVerbs = false } = req . body ;
const results = await brainy . search ( vector , k , {
nounTypes ,
includeVerbs ,
searchMode : 'local' // Force local search for security
} ) ;
res . json ( {
results ,
query : {
vectorLength : vector . length ,
k ,
nounTypes ,
includeVerbs
} ,
timestamp : new Date ( ) . toISOString ( )
} ) ;
}
catch ( error ) {
console . error ( 'Search failed:' , error ) ;
res . status ( 500 ) . json ( {
error : 'Search failed' ,
message : error . message
} ) ;
}
} ) ;
// Text search endpoint
app . post ( '/api/search/text' , [
body ( 'query' ) . isString ( ) . isLength ( { min : 1 , max : 1000 } ) . withMessage ( 'Query must be a string between 1 and 1000 characters' ) ,
body ( 'k' ) . optional ( ) . isInt ( { min : 1 , max : 100 } ) . withMessage ( 'k must be between 1 and 100' ) ,
body ( 'nounTypes' ) . optional ( ) . isArray ( ) . withMessage ( 'nounTypes must be an array' ) ,
body ( 'includeVerbs' ) . optional ( ) . isBoolean ( ) . withMessage ( 'includeVerbs must be boolean' ) ,
handleValidationErrors
] , async ( req , res ) => {
try {
const brainy = await initializeBrainy ( ) ;
const { query , k = 10 , nounTypes , includeVerbs = false } = req . body ;
const results = await brainy . searchText ( query , k , {
nounTypes ,
includeVerbs
} ) ;
res . json ( {
results ,
query : {
text : query ,
k ,
nounTypes ,
includeVerbs
} ,
timestamp : new Date ( ) . toISOString ( )
} ) ;
}
catch ( error ) {
console . error ( 'Text search failed:' , error ) ;
res . status ( 500 ) . json ( {
error : 'Text search failed' ,
message : error . message
} ) ;
}
} ) ;
// Get item by ID
app . get ( '/api/item/:id' , [
param ( 'id' ) . isString ( ) . isLength ( { min : 1 , max : 100 } ) . withMessage ( 'ID must be a string between 1 and 100 characters' ) ,
handleValidationErrors
] , async ( req , res ) => {
try {
const brainy = await initializeBrainy ( ) ;
const { id } = req . params ;
const item = await brainy . get ( id ) ;
if ( ! item ) {
return res . status ( 404 ) . json ( {
error : 'Item not found' ,
id
} ) ;
}
res . json ( {
item ,
timestamp : new Date ( ) . toISOString ( )
} ) ;
}
catch ( error ) {
console . error ( 'Get item failed:' , error ) ;
res . status ( 500 ) . json ( {
error : 'Failed to get item' ,
message : error . message
} ) ;
}
} ) ;
// Get all items (paginated)
app . get ( '/api/items' , [
query ( 'page' ) . optional ( ) . isInt ( { min : 1 } ) . withMessage ( 'Page must be a positive integer' ) ,
query ( 'limit' ) . optional ( ) . isInt ( { min : 1 , max : 100 } ) . withMessage ( 'Limit must be between 1 and 100' ) ,
handleValidationErrors
] , async ( req , res ) => {
try {
const brainy = await initializeBrainy ( ) ;
const page = parseInt ( req . query . page ) || 1 ;
const limit = parseInt ( req . query . limit ) || 20 ;
const allNouns = await brainy . getAllNouns ( ) ;
const total = allNouns . length ;
const startIndex = ( page - 1 ) * limit ;
const endIndex = startIndex + limit ;
const items = allNouns . slice ( startIndex , endIndex ) ;
res . json ( {
items ,
pagination : {
page ,
limit ,
total ,
totalPages : Math . ceil ( total / limit ) ,
hasNext : endIndex < total ,
hasPrev : page > 1
} ,
timestamp : new Date ( ) . toISOString ( )
} ) ;
}
catch ( error ) {
console . error ( 'Get items failed:' , error ) ;
res . status ( 500 ) . json ( {
error : 'Failed to get items' ,
message : error . message
} ) ;
}
} ) ;
// Find similar items
app . post ( '/api/similar/:id' , [
param ( 'id' ) . isString ( ) . isLength ( { min : 1 , max : 100 } ) . withMessage ( 'ID must be a string between 1 and 100 characters' ) ,
body ( 'k' ) . optional ( ) . isInt ( { min : 1 , max : 100 } ) . withMessage ( 'k must be between 1 and 100' ) ,
body ( 'includeVerbs' ) . optional ( ) . isBoolean ( ) . withMessage ( 'includeVerbs must be boolean' ) ,
handleValidationErrors
] , async ( req , res ) => {
try {
const brainy = await initializeBrainy ( ) ;
const { id } = req . params ;
const { k = 10 , includeVerbs = false } = req . body ;
const results = await brainy . findSimilar ( id , {
limit : k ,
includeVerbs
} ) ;
res . json ( {
results ,
query : {
id ,
k ,
includeVerbs
} ,
timestamp : new Date ( ) . toISOString ( )
} ) ;
}
catch ( error ) {
console . error ( 'Find similar failed:' , error ) ;
res . status ( 500 ) . json ( {
error : 'Failed to find similar items' ,
message : error . message
} ) ;
}
} ) ;
// 404 handler
app . use ( '*' , ( req , res ) => {
res . status ( 404 ) . json ( {
error : 'Endpoint not found' ,
path : req . originalUrl ,
method : req . method
} ) ;
} ) ;
// Global error handler
app . use ( ( err , req , res , next ) => {
console . error ( 'Unhandled error:' , err ) ;
res . status ( 500 ) . json ( {
error : 'Internal server error' ,
message : process . env . NODE _ENV === 'development' ? err . message : 'Something went wrong'
} ) ;
} ) ;
// Graceful shutdown
process . on ( 'SIGTERM' , async ( ) => {
console . log ( 'SIGTERM received, shutting down gracefully...' ) ;
if ( brainyInstance ) {
await brainyInstance . shutDown ( ) ;
}
process . exit ( 0 ) ;
} ) ;
process . on ( 'SIGINT' , async ( ) => {
console . log ( 'SIGINT received, shutting down gracefully...' ) ;
if ( brainyInstance ) {
await brainyInstance . shutDown ( ) ;
}
process . exit ( 0 ) ;
} ) ;
// Start server
async function startServer ( ) {
try {
// Initialize Brainy on startup
await initializeBrainy ( ) ;
app . listen ( Number ( PORT ) , HOST , ( ) => {
console . log ( ` 🚀 Brainy Web Service started ` ) ;
console . log ( ` 📍 Server running on http:// ${ HOST } : ${ PORT } ` ) ;
console . log ( ` 📊 API documentation available at http:// ${ HOST } : ${ PORT } /api ` ) ;
console . log ( ` 🔒 Read-only mode: ${ brainyInstance ? . isReadOnly ( ) } ` ) ;
console . log ( ` 📁 Data path: ${ DATA _PATH } ` ) ;
} ) ;
}
catch ( error ) {
console . error ( 'Failed to start server:' , error ) ;
process . exit ( 1 ) ;
}
}
// Start the server
startServer ( ) . catch ( console . error ) ;
//# sourceMappingURL=server.js.map