mirror of
https://github.com/Lifeforge-app/lifeforge.git
synced 2026-06-28 06:46:24 +00:00
feat(server): implement database connection and collection validation during server starting process
Former-commit-id: 68c3a1e5867c1b34f4e439a6735e4d094c4aef08 [formerly c356ba449e4b90d8a585a4fe1d5509f3f5a9f77c] [formerly f722aab091f6d05405b65d46c9c4fbbefe856f5c [formerly 555c7a1db5910c3b0187357202a7663db72a5489]] Former-commit-id: 6ba8b762e82e176a08e0dd1924c81ce66d4f6546 [formerly 1738ea41e2ab3e4134aee976befbef5d0f261512] Former-commit-id: e72ccd9862d331cfc51f3d5f2564bdec134d4dc1
This commit is contained in:
@@ -20,13 +20,6 @@ if (!process.env.PB_HOST || !process.env.PB_EMAIL || !process.env.PB_PASSWORD) {
|
||||
|
||||
const pb = new Pocketbase(process.env.PB_HOST)
|
||||
|
||||
let MAIN_SCHEMA_EXPORTS = `import flattenSchemas from '@functions/utils/flattenSchema'
|
||||
|
||||
export const SCHEMAS = {
|
||||
`
|
||||
|
||||
const moduleSchemas: Record<string, string> = {}
|
||||
|
||||
try {
|
||||
await pb
|
||||
.collection('_superusers')
|
||||
@@ -41,6 +34,13 @@ try {
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
let MAIN_SCHEMA_EXPORTS = `import flattenSchemas from '@functions/utils/flattenSchema'
|
||||
|
||||
export const SCHEMAS = {
|
||||
`
|
||||
|
||||
const moduleSchemas: Record<string, string> = {}
|
||||
|
||||
const allModules = fs.readdirSync('./server/src/lib', { withFileTypes: true })
|
||||
|
||||
const modulesMap: Record<string, CollectionModel[]> = {}
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import cors from 'cors'
|
||||
import dotenv from 'dotenv'
|
||||
import express from 'express'
|
||||
import helmet from 'helmet'
|
||||
|
||||
@@ -8,10 +7,6 @@ import rateLimitingMiddleware from './middlewares/rateLimitingMiddleware'
|
||||
import router from './routes'
|
||||
import { CORS_ALLOWED_ORIGINS } from './routes/constants/corsAllowedOrigins'
|
||||
|
||||
dotenv.config({
|
||||
path: './env/.env.local'
|
||||
})
|
||||
|
||||
const app = express()
|
||||
|
||||
// Security headers
|
||||
|
||||
171
server/src/core/functions/utils/checkDB.ts
Normal file
171
server/src/core/functions/utils/checkDB.ts
Normal file
@@ -0,0 +1,171 @@
|
||||
import { LoggingService } from '@functions/logging/loggingService'
|
||||
import COLLECTION_SCHEMAS from '@schema'
|
||||
import Pocketbase from 'pocketbase'
|
||||
|
||||
interface DBConnectionConfig {
|
||||
host: string
|
||||
email: string
|
||||
password: string
|
||||
}
|
||||
|
||||
interface CollectionValidationResult {
|
||||
isValid: boolean
|
||||
missingCollections: string[]
|
||||
totalCollections: number
|
||||
}
|
||||
|
||||
class DatabaseConnectionError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly cause?: Error
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'DatabaseConnectionError'
|
||||
}
|
||||
}
|
||||
|
||||
class DatabaseValidationError extends Error {
|
||||
constructor(
|
||||
message: string,
|
||||
public readonly missingCollections: string[]
|
||||
) {
|
||||
super(message)
|
||||
this.name = 'DatabaseValidationError'
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates the required environment variables for database connection
|
||||
* @throws {DatabaseConnectionError} When required environment variables are missing
|
||||
*/
|
||||
function validateEnvironmentVariables(): DBConnectionConfig {
|
||||
const { PB_HOST, PB_EMAIL, PB_PASSWORD } = process.env
|
||||
|
||||
if (!PB_HOST || !PB_EMAIL || !PB_PASSWORD) {
|
||||
throw new DatabaseConnectionError(
|
||||
'Missing required environment variables: PB_HOST, PB_EMAIL, and PB_PASSWORD must be provided'
|
||||
)
|
||||
}
|
||||
|
||||
return { host: PB_HOST, email: PB_EMAIL, password: PB_PASSWORD }
|
||||
}
|
||||
|
||||
/**
|
||||
* Establishes and validates connection to PocketBase with superuser privileges
|
||||
* @param config Database connection configuration
|
||||
* @returns Authenticated PocketBase instance
|
||||
* @throws {DatabaseConnectionError} When connection or authentication fails
|
||||
*/
|
||||
async function connectToPocketBase(
|
||||
config: DBConnectionConfig
|
||||
): Promise<Pocketbase> {
|
||||
const pb = new Pocketbase(config.host)
|
||||
|
||||
try {
|
||||
await pb
|
||||
.collection('_superusers')
|
||||
.authWithPassword(config.email, config.password)
|
||||
|
||||
if (!pb.authStore.isSuperuser || !pb.authStore.isValid) {
|
||||
throw new DatabaseConnectionError(
|
||||
'Authentication failed: Invalid credentials or insufficient privileges'
|
||||
)
|
||||
}
|
||||
|
||||
LoggingService.info('Successfully connected to PocketBase', 'DB')
|
||||
|
||||
return pb
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : 'Unknown error'
|
||||
|
||||
throw new DatabaseConnectionError(
|
||||
`Failed to connect to PocketBase: ${errorMessage}`,
|
||||
error instanceof Error ? error : undefined
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Maps collection names to their target names in PocketBase
|
||||
* Handles special cases like users__users -> users
|
||||
*/
|
||||
function mapCollectionName(collectionName: string): string {
|
||||
return collectionName === 'users__users' ? 'users' : collectionName
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates that all required collections exist in PocketBase
|
||||
* @param pb Authenticated PocketBase instance
|
||||
* @returns Validation result with details about missing collections
|
||||
*/
|
||||
async function validateCollections(
|
||||
pb: Pocketbase
|
||||
): Promise<CollectionValidationResult> {
|
||||
const allCollections = await pb.collections.getFullList()
|
||||
|
||||
const existingCollectionNames = new Set(allCollections.map(c => c.name))
|
||||
|
||||
const requiredCollections = Object.keys(COLLECTION_SCHEMAS)
|
||||
|
||||
const missingCollections: string[] = []
|
||||
|
||||
for (const collection of requiredCollections) {
|
||||
const targetCollection = mapCollectionName(collection)
|
||||
|
||||
if (!existingCollectionNames.has(targetCollection)) {
|
||||
missingCollections.push(collection)
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
isValid: missingCollections.length === 0,
|
||||
missingCollections,
|
||||
totalCollections: requiredCollections.length
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates database connection and schema integrity
|
||||
* Ensures PocketBase is accessible and contains all required collections
|
||||
*
|
||||
* @throws {DatabaseConnectionError} When connection fails
|
||||
* @throws {DatabaseValidationError} When required collections are missing
|
||||
*/
|
||||
export default async function checkDB(): Promise<void> {
|
||||
try {
|
||||
// Validate environment configuration
|
||||
const config = validateEnvironmentVariables()
|
||||
|
||||
// Establish database connection
|
||||
const pb = await connectToPocketBase(config)
|
||||
|
||||
// Validate collection schema
|
||||
const validationResult = await validateCollections(pb)
|
||||
|
||||
if (!validationResult.isValid) {
|
||||
throw new DatabaseValidationError(
|
||||
`Missing collections in PocketBase: ${validationResult.missingCollections.join(', ')}`,
|
||||
validationResult.missingCollections
|
||||
)
|
||||
}
|
||||
|
||||
LoggingService.info(
|
||||
`Database validation complete. All ${validationResult.totalCollections} collections are present`,
|
||||
'DB'
|
||||
)
|
||||
} catch (error) {
|
||||
if (
|
||||
error instanceof DatabaseConnectionError ||
|
||||
error instanceof DatabaseValidationError
|
||||
) {
|
||||
LoggingService.error(error.message, 'DB')
|
||||
} else {
|
||||
LoggingService.error(
|
||||
`Unexpected error during database validation: ${error}`,
|
||||
'DB'
|
||||
)
|
||||
}
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
@@ -1,11 +1,26 @@
|
||||
import { LoggingService } from '@functions/logging/loggingService'
|
||||
import { setupSocket } from '@functions/socketio/setupSocket'
|
||||
import checkDB from '@functions/utils/checkDB'
|
||||
import traceRouteStack from '@functions/utils/traceRouteStack'
|
||||
import dotenv from 'dotenv'
|
||||
import { createServer } from 'node:http'
|
||||
import { Server } from 'socket.io'
|
||||
|
||||
import app from './core/app'
|
||||
|
||||
dotenv.config({
|
||||
path: './env/.env.local'
|
||||
})
|
||||
|
||||
if (!process.env.MASTER_KEY) {
|
||||
LoggingService.error(
|
||||
'Please provide MASTER_KEY in your environment variables.'
|
||||
)
|
||||
process.exit(1)
|
||||
}
|
||||
|
||||
await checkDB()
|
||||
|
||||
const server = createServer(app)
|
||||
|
||||
const io = new Server(server)
|
||||
@@ -19,6 +34,9 @@ app.request.io = io
|
||||
server.listen(process.env.PORT, () => {
|
||||
const routes = traceRouteStack(app._router.stack)
|
||||
|
||||
LoggingService.debug(`Registered routes: ${routes.length}`)
|
||||
LoggingService.info(`REST API server running on port ${process.env.PORT}`)
|
||||
LoggingService.debug(`Registered routes: ${routes.length}`, 'API')
|
||||
LoggingService.info(
|
||||
`REST API server running on port ${process.env.PORT}`,
|
||||
'API'
|
||||
)
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user