generated from alphane/template
Initial commit
This commit is contained in:
@@ -0,0 +1,103 @@
|
||||
import knex, { type Knex } from "knex"
|
||||
|
||||
import {
|
||||
DB_HEALTH_CHECK_INTERVAL_SECONDS,
|
||||
DB_HEALTH_CHECK_RETRIES,
|
||||
DB_HEALTH_CHECK_START_PERIOD_SECONDS,
|
||||
DB_HEALTH_CHECK_TIMEOUT_SECONDS,
|
||||
} from "@/config"
|
||||
import logger from "@/utils/logger"
|
||||
import sleep from "@/utils/sleep"
|
||||
import {
|
||||
isCredentialFailure,
|
||||
isNetworkFailure,
|
||||
isSocketFailure,
|
||||
isMissingDatabaseFailure,
|
||||
} from "@/utils/db-error-helpers"
|
||||
import { buildKnexConfig } from "@/db/db-migration-client"
|
||||
|
||||
function checkHealth(dbMigrationClient: Knex, timeoutSeconds: number) {
|
||||
return new Promise((resolve, reject) => {
|
||||
const timer = setTimeout(() => reject(new Error("Connection timeout")), timeoutSeconds * 1000)
|
||||
dbMigrationClient
|
||||
.raw("SELECT 1")
|
||||
.then(() => {
|
||||
clearTimeout(timer)
|
||||
resolve(null)
|
||||
})
|
||||
.catch(reject)
|
||||
})
|
||||
}
|
||||
|
||||
export async function waitForDatabase({
|
||||
intervalSeconds = DB_HEALTH_CHECK_INTERVAL_SECONDS,
|
||||
timeoutSeconds = DB_HEALTH_CHECK_TIMEOUT_SECONDS,
|
||||
retries = DB_HEALTH_CHECK_RETRIES,
|
||||
startPeriodSeconds = DB_HEALTH_CHECK_START_PERIOD_SECONDS,
|
||||
}: {
|
||||
intervalSeconds?: number
|
||||
timeoutSeconds?: number
|
||||
retries?: number
|
||||
startPeriodSeconds?: number
|
||||
} = {}): Promise<void> {
|
||||
await sleep(startPeriodSeconds)
|
||||
|
||||
logger.info("Attempting direct to database connection...")
|
||||
const databaseConfig = buildKnexConfig()
|
||||
|
||||
let dbMigrationClient = knex(databaseConfig)
|
||||
let isDatabaseSocketReady = false
|
||||
|
||||
for (let i = 0; i < retries; i++) {
|
||||
try {
|
||||
await checkHealth(dbMigrationClient, timeoutSeconds)
|
||||
logger.info("Database connection successful.")
|
||||
return
|
||||
} catch (error) {
|
||||
if (isSocketFailure(error)) {
|
||||
logger.info(`Database socket is not ready, retrying... ${error}`, { error })
|
||||
await sleep(intervalSeconds)
|
||||
} else if (isNetworkFailure(error)) {
|
||||
logger.info(`Network error, retrying... ${error}`, { error })
|
||||
await sleep(intervalSeconds)
|
||||
} else if (isCredentialFailure(error)) {
|
||||
if (isDatabaseSocketReady) {
|
||||
logger.error(`Database connection failed due to invalid credentials: ${error}`, { error })
|
||||
throw error
|
||||
} else {
|
||||
logger.info(
|
||||
"Falling back to database server-level connection (database might not exist)..."
|
||||
)
|
||||
const serverLevelConfig = buildKnexConfig({ connection: { database: "" } })
|
||||
dbMigrationClient = knex(serverLevelConfig)
|
||||
i -= 1
|
||||
isDatabaseSocketReady = true
|
||||
continue
|
||||
}
|
||||
} else if (isMissingDatabaseFailure(error)) {
|
||||
if (isDatabaseSocketReady) {
|
||||
logger.error(`Database connection failed because database does not exist): ${error}`, {
|
||||
error,
|
||||
})
|
||||
throw error
|
||||
} else {
|
||||
logger.info(
|
||||
"Falling back to default postgres connection (after database does not exist failure)..."
|
||||
)
|
||||
const serverLevelConfig = buildKnexConfig({ connection: { database: "postgres" } })
|
||||
dbMigrationClient = knex(serverLevelConfig)
|
||||
i -= 1
|
||||
isDatabaseSocketReady = true
|
||||
continue
|
||||
}
|
||||
} else {
|
||||
logger.error(`Unknown database connection error: ${error}`, { error })
|
||||
//throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`Failed to connect to the database due to timeout.`)
|
||||
}
|
||||
|
||||
export default waitForDatabase
|
||||
@@ -0,0 +1,71 @@
|
||||
import knex, { type Knex } from "knex"
|
||||
|
||||
import { logger } from "@/utils/logger"
|
||||
import { isCredentialFailure, isMissingDatabaseFailure } from "@/utils/db-error-helpers"
|
||||
import { buildKnexConfig } from "@/db/db-migration-client"
|
||||
import { DB_DATABASE } from "@/config"
|
||||
|
||||
async function databaseExists(dbMigrationClient: Knex, databaseName: string): Promise<boolean> {
|
||||
const result = await dbMigrationClient.raw("SELECT 1 FROM pg_database WHERE datname = ?", [
|
||||
databaseName,
|
||||
])
|
||||
|
||||
return result.rows.length > 0
|
||||
}
|
||||
|
||||
async function createDatabase(): Promise<true> {
|
||||
logger.info("Attempting direct to database connection to determine if database exists...")
|
||||
const databaseConfig = buildKnexConfig()
|
||||
let dbMigrationClient = knex(databaseConfig)
|
||||
let isCredentialFailureError = false
|
||||
let isMissingDatabaseError = false
|
||||
|
||||
try {
|
||||
if (await databaseExists(dbMigrationClient, DB_DATABASE)) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
if (isCredentialFailure(error)) {
|
||||
isCredentialFailureError = true
|
||||
logger.info("Database connection failed due to invalid credential, retrying...")
|
||||
}
|
||||
if (isMissingDatabaseFailure(error)) {
|
||||
isMissingDatabaseError = true
|
||||
logger.info("Database connection failed due missing default database, retrying...")
|
||||
} else {
|
||||
logger.error(`Unknown connection failure, could not determine if database exists: ${error}`, {
|
||||
error,
|
||||
})
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
if (isCredentialFailureError || isMissingDatabaseError) {
|
||||
logger.info("Attempting server-level connection to determine if database exists...")
|
||||
const serverLevelConfig = buildKnexConfig({ connection: { database: "" } })
|
||||
dbMigrationClient = knex(serverLevelConfig)
|
||||
try {
|
||||
if (await databaseExists(dbMigrationClient, DB_DATABASE)) {
|
||||
return true
|
||||
}
|
||||
} catch (error) {
|
||||
logger.error(
|
||||
`Could not determine if database exists database with server-level connection: ${error}`,
|
||||
{ error }
|
||||
)
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
logger.info(`Database ${DB_DATABASE} does not exist: creating...`)
|
||||
try {
|
||||
await dbMigrationClient.raw(`CREATE DATABASE ${DB_DATABASE}`)
|
||||
} catch (error) {
|
||||
logger.error(`Failed to create database: ${error}`, { error })
|
||||
throw error
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
export default createDatabase
|
||||
@@ -0,0 +1,31 @@
|
||||
import dbMigrationClient from "@/db/db-migration-client"
|
||||
import { logger } from "@/utils/logger"
|
||||
|
||||
type MigrationInfo = {
|
||||
file: string
|
||||
directory: string
|
||||
}
|
||||
|
||||
async function runMigrations(): Promise<void> {
|
||||
const [_completedMigrations, pendingMigrations]: [MigrationInfo[], MigrationInfo[]] =
|
||||
await dbMigrationClient.migrate.list()
|
||||
|
||||
if (pendingMigrations.length === 0) {
|
||||
logger.info("No pending migrations.")
|
||||
return
|
||||
}
|
||||
|
||||
for (const { file, directory } of pendingMigrations) {
|
||||
logger.info(`Running migration: ${directory}/${file}`)
|
||||
try {
|
||||
await dbMigrationClient.migrate.up()
|
||||
} catch (error) {
|
||||
logger.error(`Error running migration: ${error}`, { error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
logger.info("All migrations completed successfully.")
|
||||
}
|
||||
|
||||
export default runMigrations
|
||||
@@ -0,0 +1,25 @@
|
||||
import { logger } from "@/utils/logger"
|
||||
import dbMigrationClient from "@/db/db-migration-client"
|
||||
import { User } from "@/models"
|
||||
|
||||
export async function runSeeds(): Promise<void> {
|
||||
if (process.env.SKIP_SEEDING_UNLESS_EMPTY === "true") {
|
||||
const count = await User.count({ logging: false })
|
||||
|
||||
if (count > 0) {
|
||||
logger.warn("Skipping seeding as SKIP_SEEDING_UNLESS_EMPTY set, and data already seeded.")
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await dbMigrationClient.seed.run()
|
||||
} catch (error) {
|
||||
logger.error(`Error running seeds: ${error}`, { error })
|
||||
throw error
|
||||
}
|
||||
|
||||
return
|
||||
}
|
||||
|
||||
export default runSeeds
|
||||
@@ -0,0 +1,39 @@
|
||||
import { logger } from "@/utils/logger"
|
||||
import * as fs from "fs/promises"
|
||||
import * as path from "path"
|
||||
|
||||
const NON_INITIALIZER_REGEX = /^index\.(ts|js)$/
|
||||
|
||||
export async function importAndExecuteInitializers() {
|
||||
const files = await fs.readdir(__dirname)
|
||||
|
||||
for (const file of files) {
|
||||
if (NON_INITIALIZER_REGEX.test(file)) continue
|
||||
|
||||
const modulePath = path.join(__dirname, file)
|
||||
logger.info(`Running initializer: ${modulePath}`)
|
||||
|
||||
try {
|
||||
const { default: initializerAction } = await require(modulePath)
|
||||
await initializerAction()
|
||||
} catch (error) {
|
||||
logger.error(`Failed to run initializer: ${modulePath}`, { error })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
if (require.main === module) {
|
||||
// TODO: add some kind of middleware that 503s? if initialization failed?
|
||||
;(async () => {
|
||||
try {
|
||||
await importAndExecuteInitializers()
|
||||
} catch {
|
||||
logger.error("Failed to complete initialization!")
|
||||
}
|
||||
|
||||
process.exit(0)
|
||||
})()
|
||||
}
|
||||
Reference in New Issue
Block a user