|
|
import 'reflect-metadata'; |
|
|
import { DataSource, DataSourceOptions } from 'typeorm'; |
|
|
import entities from './entities/index.js'; |
|
|
import { registerPostgresVectorType } from './types/postgresVectorType.js'; |
|
|
import { VectorEmbeddingSubscriber } from './subscribers/VectorEmbeddingSubscriber.js'; |
|
|
import { getSmartRoutingConfig } from '../utils/smartRouting.js'; |
|
|
|
|
|
|
|
|
const createRequiredExtensions = async (dataSource: DataSource): Promise<void> => { |
|
|
try { |
|
|
await dataSource.query('CREATE EXTENSION IF NOT EXISTS "uuid-ossp";'); |
|
|
console.log('UUID extension created or already exists.'); |
|
|
} catch (err: any) { |
|
|
console.warn('Failed to create uuid-ossp extension:', err.message); |
|
|
console.warn('UUID generation functionality may not be available.'); |
|
|
} |
|
|
|
|
|
try { |
|
|
await dataSource.query('CREATE EXTENSION IF NOT EXISTS vector;'); |
|
|
console.log('Vector extension created or already exists.'); |
|
|
} catch (err: any) { |
|
|
console.warn('Failed to create vector extension:', err.message); |
|
|
console.warn('Vector functionality may not be available.'); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const getDatabaseUrl = (): string => { |
|
|
return getSmartRoutingConfig().dbUrl; |
|
|
}; |
|
|
|
|
|
|
|
|
const defaultConfig: DataSourceOptions = { |
|
|
type: 'postgres', |
|
|
url: getDatabaseUrl(), |
|
|
synchronize: true, |
|
|
entities: entities, |
|
|
subscribers: [VectorEmbeddingSubscriber], |
|
|
}; |
|
|
|
|
|
|
|
|
let appDataSource = new DataSource(defaultConfig); |
|
|
|
|
|
|
|
|
let initializationPromise: Promise<DataSource> | null = null; |
|
|
|
|
|
|
|
|
export const updateDataSourceConfig = (): DataSource => { |
|
|
const newConfig: DataSourceOptions = { |
|
|
...defaultConfig, |
|
|
url: getDatabaseUrl(), |
|
|
}; |
|
|
|
|
|
|
|
|
const currentUrl = (appDataSource.options as any).url; |
|
|
if (currentUrl !== newConfig.url) { |
|
|
console.log('Database URL configuration changed, updating DataSource...'); |
|
|
appDataSource = new DataSource(newConfig); |
|
|
|
|
|
initializationPromise = null; |
|
|
} |
|
|
|
|
|
return appDataSource; |
|
|
}; |
|
|
|
|
|
|
|
|
export const getAppDataSource = (): DataSource => { |
|
|
return appDataSource; |
|
|
}; |
|
|
|
|
|
|
|
|
export const reconnectDatabase = async (): Promise<DataSource> => { |
|
|
try { |
|
|
|
|
|
if (appDataSource.isInitialized) { |
|
|
console.log('Closing existing database connection...'); |
|
|
await appDataSource.destroy(); |
|
|
} |
|
|
|
|
|
|
|
|
initializationPromise = null; |
|
|
|
|
|
|
|
|
appDataSource = updateDataSourceConfig(); |
|
|
return await initializeDatabase(); |
|
|
} catch (error) { |
|
|
console.error('Error during database reconnection:', error); |
|
|
throw error; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
export const initializeDatabase = async (): Promise<DataSource> => { |
|
|
|
|
|
if (initializationPromise) { |
|
|
console.log('Database initialization already in progress, waiting for completion...'); |
|
|
return initializationPromise; |
|
|
} |
|
|
|
|
|
|
|
|
if (appDataSource.isInitialized) { |
|
|
console.log('Database already initialized, returning existing instance'); |
|
|
return Promise.resolve(appDataSource); |
|
|
} |
|
|
|
|
|
|
|
|
initializationPromise = performDatabaseInitialization(); |
|
|
|
|
|
try { |
|
|
const result = await initializationPromise; |
|
|
console.log('Database initialization completed successfully'); |
|
|
return result; |
|
|
} catch (error) { |
|
|
|
|
|
initializationPromise = null; |
|
|
console.error('Database initialization failed:', error); |
|
|
throw error; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
const performDatabaseInitialization = async (): Promise<DataSource> => { |
|
|
try { |
|
|
|
|
|
appDataSource = updateDataSourceConfig(); |
|
|
|
|
|
if (!appDataSource.isInitialized) { |
|
|
console.log('Initializing database connection...'); |
|
|
|
|
|
await appDataSource.initialize(); |
|
|
registerPostgresVectorType(appDataSource); |
|
|
|
|
|
|
|
|
await createRequiredExtensions(appDataSource); |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const tableExists = await appDataSource.query(` |
|
|
SELECT EXISTS ( |
|
|
SELECT FROM information_schema.tables |
|
|
WHERE table_schema = 'public' |
|
|
AND table_name = 'vector_embeddings' |
|
|
); |
|
|
`); |
|
|
|
|
|
if (tableExists[0].exists) { |
|
|
|
|
|
console.log('Configuring vector support for embeddings table...'); |
|
|
|
|
|
|
|
|
try { |
|
|
await appDataSource.query(`DROP INDEX IF EXISTS idx_vector_embeddings_embedding;`); |
|
|
} catch (dropError: any) { |
|
|
console.warn('Note: Could not drop existing index:', dropError.message); |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const columnType = await appDataSource.query(` |
|
|
SELECT data_type FROM information_schema.columns |
|
|
WHERE table_schema = 'public' AND table_name = 'vector_embeddings' |
|
|
AND column_name = 'embedding'; |
|
|
`); |
|
|
|
|
|
if (columnType.length > 0 && columnType[0].data_type !== 'vector') { |
|
|
await appDataSource.query(` |
|
|
ALTER TABLE vector_embeddings |
|
|
ALTER COLUMN embedding TYPE vector USING embedding::vector; |
|
|
`); |
|
|
console.log('Vector embedding column type updated successfully.'); |
|
|
} |
|
|
} catch (alterError: any) { |
|
|
console.warn('Could not alter embedding column type:', alterError.message); |
|
|
console.warn('Will try to recreate the table later.'); |
|
|
} |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const records = await appDataSource.query(` |
|
|
SELECT dimensions FROM vector_embeddings LIMIT 1; |
|
|
`); |
|
|
|
|
|
let dimensions = 1536; |
|
|
if (records && records.length > 0 && records[0].dimensions) { |
|
|
dimensions = records[0].dimensions; |
|
|
console.log(`Found vector dimension from existing data: ${dimensions}`); |
|
|
} else { |
|
|
console.log(`Using default vector dimension: ${dimensions} (no existing data found)`); |
|
|
} |
|
|
|
|
|
|
|
|
if (records && records.length > 0) { |
|
|
await appDataSource.query(` |
|
|
ALTER TABLE vector_embeddings |
|
|
ALTER COLUMN embedding TYPE vector(${dimensions}); |
|
|
`); |
|
|
|
|
|
|
|
|
await appDataSource.query(` |
|
|
CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding |
|
|
ON vector_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); |
|
|
`); |
|
|
console.log('Created IVFFlat index for vector similarity search.'); |
|
|
} else { |
|
|
console.log( |
|
|
'No existing vector data found, skipping index creation - will be handled by vector service.', |
|
|
); |
|
|
} |
|
|
} catch (indexError: any) { |
|
|
console.warn('IVFFlat index creation failed:', indexError.message); |
|
|
console.warn('Trying alternative index type...'); |
|
|
|
|
|
try { |
|
|
|
|
|
await appDataSource.query(` |
|
|
CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding |
|
|
ON vector_embeddings USING hnsw (embedding vector_cosine_ops); |
|
|
`); |
|
|
console.log('Created HNSW index for vector similarity search.'); |
|
|
} catch (hnswError: any) { |
|
|
|
|
|
console.warn('HNSW index creation failed too. Using simple L2 distance index.'); |
|
|
|
|
|
try { |
|
|
|
|
|
await appDataSource.query(` |
|
|
CREATE INDEX IF NOT EXISTS idx_vector_embeddings_embedding |
|
|
ON vector_embeddings USING gin (embedding); |
|
|
`); |
|
|
console.log('Created GIN index for basic vector lookups.'); |
|
|
} catch (ginError: any) { |
|
|
console.warn('All index creation attempts failed:', ginError.message); |
|
|
console.warn('Vector search will be slower without an optimized index.'); |
|
|
} |
|
|
} |
|
|
} |
|
|
} else { |
|
|
console.log( |
|
|
'Vector embeddings table does not exist yet - will configure after schema sync.', |
|
|
); |
|
|
} |
|
|
} catch (error: any) { |
|
|
console.warn('Could not set up vector column/index:', error.message); |
|
|
console.warn('Will attempt again after schema synchronization.'); |
|
|
} |
|
|
|
|
|
console.log('Database connection established successfully.'); |
|
|
|
|
|
|
|
|
if (defaultConfig.synchronize) { |
|
|
try { |
|
|
console.log('Running final vector configuration check...'); |
|
|
|
|
|
|
|
|
const tableExists = await appDataSource.query(` |
|
|
SELECT EXISTS ( |
|
|
SELECT FROM information_schema.tables |
|
|
WHERE table_schema = 'public' |
|
|
AND table_name = 'vector_embeddings' |
|
|
); |
|
|
`); |
|
|
|
|
|
if (tableExists[0].exists) { |
|
|
console.log('Vector embeddings table found, checking configuration...'); |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
const records = await appDataSource.query(` |
|
|
SELECT dimensions FROM vector_embeddings LIMIT 1; |
|
|
`); |
|
|
|
|
|
|
|
|
if (records && records.length > 0 && records[0].dimensions) { |
|
|
const dimensions = records[0].dimensions; |
|
|
console.log(`Found vector dimension from database: ${dimensions}`); |
|
|
|
|
|
|
|
|
await appDataSource.query(` |
|
|
ALTER TABLE vector_embeddings |
|
|
ALTER COLUMN embedding TYPE vector(${dimensions}); |
|
|
`); |
|
|
console.log('Vector embedding column type updated in final check.'); |
|
|
|
|
|
|
|
|
try { |
|
|
|
|
|
await appDataSource.query(` |
|
|
DROP INDEX IF EXISTS idx_vector_embeddings_embedding; |
|
|
`); |
|
|
|
|
|
|
|
|
await appDataSource.query(` |
|
|
CREATE INDEX idx_vector_embeddings_embedding |
|
|
ON vector_embeddings USING ivfflat (embedding vector_cosine_ops) WITH (lists = 100); |
|
|
`); |
|
|
console.log('Created IVFFlat index in final check.'); |
|
|
} catch (indexError: any) { |
|
|
console.warn('Final index creation attempt did not succeed:', indexError.message); |
|
|
console.warn('Using basic lookup without vector index.'); |
|
|
} |
|
|
} else { |
|
|
console.log( |
|
|
'No existing vector data found, vector dimensions will be configured by vector service.', |
|
|
); |
|
|
} |
|
|
} catch (setupError: any) { |
|
|
console.warn('Vector setup in final check failed:', setupError.message); |
|
|
} |
|
|
} |
|
|
} catch (error: any) { |
|
|
console.warn('Post-initialization vector setup failed:', error.message); |
|
|
} |
|
|
} |
|
|
} |
|
|
return appDataSource; |
|
|
} catch (error) { |
|
|
console.error('Error during database initialization:', error); |
|
|
throw error; |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
export const isDatabaseConnected = (): boolean => { |
|
|
return appDataSource.isInitialized; |
|
|
}; |
|
|
|
|
|
|
|
|
export const closeDatabase = async (): Promise<void> => { |
|
|
if (appDataSource.isInitialized) { |
|
|
await appDataSource.destroy(); |
|
|
console.log('Database connection closed.'); |
|
|
} |
|
|
}; |
|
|
|
|
|
|
|
|
export const AppDataSource = appDataSource; |
|
|
|
|
|
export default getAppDataSource; |
|
|
|