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:
melvinchia3636
2025-09-09 16:48:51 +08:00
parent 93e6848c41
commit 52a80c7768
4 changed files with 198 additions and 14 deletions

View File

@@ -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[]> = {}

View File

@@ -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

View 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)
}
}

View File

@@ -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'
)
})