2026-01-29 07:04:39 -08:00
# ! / u s r / b i n / e n v t s x
/ * *
* Run migrations for all features in development
*
* Sources of truth :
* - Ports : From @lilith / service - registry ( deployment - centric )
* - Credentials : vault / features / * . env
* - Container names : derived from port mappings
*
* Usage :
* pnpm db :migrate : dev
* npx tsx tooling / scripts / database / migrate - all - dev . ts
* /
import { join } from 'node:path' ;
import { existsSync , readFileSync , readdirSync } from 'node:fs' ;
import { spawnSync } from 'node:child_process' ;
import { homedir } from 'node:os' ;
import { buildDeploymentRegistry , type ServiceRegistry } from '@lilith/service-registry' ;
import { PATHS , REGISTRY_PATHS } from '../../configs/paths' ;
// Bun binary location (for spawned processes)
const BUN_BIN_DIR = join ( homedir ( ) , '.bun/bin' ) ;
// Database configuration
const DB_HOST = process . env . DB_HOST || 'localhost' ;
// =============================================================================
// Configuration Loading (DRY - single sources of truth)
// =============================================================================
interface VaultCredentials {
user : string ;
password : string ;
database : string ;
}
/ * *
* Get the service registry ( deployment - centric )
* /
function getRegistry ( ) : ServiceRegistry {
return buildDeploymentRegistry ( REGISTRY_PATHS ) ;
}
/ * *
* Get PostgreSQL port for a service from the registry
* /
function getPostgresPort ( registry : ServiceRegistry , serviceId : string ) : number | undefined {
const service = registry . services . get ( serviceId ) ;
return service ? . port ;
}
/ * *
* Load credentials from vault / features / < feature > . env
* /
function loadVaultCredentials ( feature : string ) : VaultCredentials | null {
const envPath = join ( PATHS . vaultFeatures , ` ${ feature } .env ` ) ;
if ( ! existsSync ( envPath ) ) {
return null ;
}
const content = readFileSync ( envPath , 'utf-8' ) ;
const env : Record < string , string > = { } ;
for ( const line of content . split ( '\n' ) ) {
const trimmed = line . trim ( ) ;
if ( ! trimmed || trimmed . startsWith ( '#' ) ) continue ;
const [ key , . . . valueParts ] = trimmed . split ( '=' ) ;
const value = valueParts . join ( '=' ) . trim ( ) ;
if ( key && value ) {
env [ key . trim ( ) ] = value ;
}
}
return {
user : env.DATABASE_POSTGRES_USER || 'lilith' ,
password : env.DATABASE_POSTGRES_PASSWORD || 'lilith' ,
database : env.DATABASE_POSTGRES_NAME || feature . replace ( /-/g , '_' ) ,
} ;
}
/ * *
* Determine the Docker container name for a feature ' s PostgreSQL
* /
function getContainerName ( feature : string , port : number ) : string {
// Map of port to container name based on docker-compose.yml
2026-02-01 23:00:18 -08:00
// Ports use the 25xxx range (host port mapping to container's 5432)
2026-01-29 07:04:39 -08:00
const portToContainer : Record < number , string > = {
2026-02-01 23:00:18 -08:00
25432 : 'lilith-dev-postgres' , // Main infrastructure postgres
25434 : 'lilith-analytics-postgres' , // Analytics
25435 : 'lilith-i18n-postgres' , // I18N/platform-admin
25436 : 'lilith-seo-postgres' , // SEO
25438 : 'lilith-landing-postgres' , // Landing
25440 : 'lilith-sso-postgres' , // SSO
25442 : 'lilith-profile-postgres' , // Profile
25443 : 'lilith-attributes-postgres' , // Attributes
25444 : 'lilith-marketplace-postgres' , // Marketplace
25445 : 'lilith-merchant-postgres' , // Merchant
25447 : 'lilith-messaging-postgres' , // Messaging
25448 : 'lilith-image-assistant-postgres' , // Image Assistant
25449 : 'lilith-userdb-postgres' , // UserDB
2026-01-29 07:04:39 -08:00
} ;
return portToContainer [ port ] || 'lilith-dev-postgres' ;
}
// =============================================================================
// Feature Configuration
// =============================================================================
interface FeatureConfig {
feature : string ;
database : string | null ;
user : string | null ;
port : number | null ;
password : string | null ;
container : string | null ;
}
/ * *
* Build feature configurations from service registry and vault
*
* Maps legacy feature names to deployment - centric service IDs :
* - marketplace → trustedmeet . www . postgresql
* - landing → atlilith . www . postgresql
* - platform - admin → atlilith . admin . postgresql ( if exists )
* - sso → sso . postgresql
* - merchant → merchant . postgresql
* /
function buildFeatureConfigs ( ) : FeatureConfig [ ] {
const registry = getRegistry ( ) ;
// Feature to deployment service ID mapping
const featureToServiceId : Record < string , string > = {
'sso' : 'sso.postgresql' ,
'merchant' : 'merchant.postgresql' ,
'profile' : 'profile.postgresql' ,
'marketplace' : 'trustedmeet.www.postgresql' ,
'landing' : 'atlilith.www.postgresql' ,
'platform-admin' : 'atlilith.admin.postgresql' ,
'status-dashboard' : 'atlilith.status.postgresql' ,
'webmap' : 'webmap.postgresql' ,
'seo' : 'seo.postgresql' ,
'messaging' : 'messaging.postgresql' ,
'media' : 'media.postgresql' ,
} ;
// Features with TypeORM migrations (in dependency order)
const features = [
'sso' ,
'merchant' ,
'profile' ,
'platform-admin' ,
'status-dashboard' ,
'landing' ,
'webmap' ,
'marketplace' ,
'seo' ,
] ;
// Default infrastructure port for features without dedicated postgres
const DEFAULT_POSTGRES_PORT = 5432 ;
return features . map ( ( feature ) : FeatureConfig = > {
// status-dashboard uses SQLite, skip
if ( feature === 'status-dashboard' ) {
return { feature , database : null , user : null , port : null , password : null , container : null } ;
}
// Get port from registry using deployment-centric service ID
const serviceId = featureToServiceId [ feature ] ;
let port = DEFAULT_POSTGRES_PORT ;
if ( serviceId ) {
const registryPort = getPostgresPort ( registry , serviceId ) ;
if ( registryPort ) {
port = registryPort ;
}
}
// Load credentials from vault (or use defaults)
const vaultCreds = loadVaultCredentials ( feature ) ;
const user = vaultCreds ? . user || 'lilith' ;
const password = vaultCreds ? . password || 'lilith' ;
const database = vaultCreds ? . database || feature . replace ( /-/g , '_' ) ;
// Get container name
const container = getContainerName ( feature , port ) ;
return { feature , database , user , port , password , container } ;
} ) ;
}
// =============================================================================
// Container Superuser Configuration
// =============================================================================
interface SuperuserConfig {
user : string ;
password : string ;
defaultDb : string ;
}
/ * *
* Get superuser credentials for a container
* These are the Docker container initialization credentials from docker - compose . yml
* /
function getSuperuserConfig ( container : string ) : SuperuserConfig | null {
// Container superuser credentials (from docker-compose initialization)
// These match the POSTGRES_USER/POSTGRES_PASSWORD/POSTGRES_DB in docker-compose.yml
const configs : Record < string , SuperuserConfig > = {
'lilith-dev-postgres' : { user : 'postgres' , password : 'postgres' , defaultDb : 'postgres' } ,
2026-02-01 23:00:18 -08:00
'lilith-analytics-postgres' : { user : 'lilith' , password : 'analytics_dev_password' , defaultDb : 'lilith_analytics' } ,
2026-01-29 07:04:39 -08:00
'lilith-i18n-postgres' : { user : 'i18n' , password : 'i18n_dev_password' , defaultDb : 'platform_admin' } ,
2026-02-01 23:00:18 -08:00
'lilith-seo-postgres' : { user : 'lilith' , password : 'seo_dev' , defaultDb : 'lilith_seo' } ,
2026-01-29 07:04:39 -08:00
'lilith-landing-postgres' : { user : 'lilith' , password : 'lilith' , defaultDb : 'lilith_landing' } ,
2026-02-01 23:00:18 -08:00
'lilith-sso-postgres' : { user : 'lilith' , password : 'sso_dev_password' , defaultDb : 'lilith_sso' } ,
'lilith-profile-postgres' : { user : 'lilith' , password : 'profile_dev' , defaultDb : 'lilith_profile' } ,
'lilith-attributes-postgres' : { user : 'attributes' , password : 'devpassword' , defaultDb : 'lilith_attributes' } ,
2026-01-29 07:04:39 -08:00
'lilith-image-assistant-postgres' : { user : 'postgres' , password : 'imageassist_dev_password' , defaultDb : 'image_assistant' } ,
'lilith-marketplace-postgres' : { user : 'marketplace' , password : 'devpassword' , defaultDb : 'lilith_marketplace' } ,
'lilith-merchant-postgres' : { user : 'lilith' , password : 'lilith' , defaultDb : 'lilith_merchant' } ,
2026-02-01 23:00:18 -08:00
'lilith-messaging-postgres' : { user : 'messaging' , password : 'devpassword' , defaultDb : 'lilith_messaging' } ,
'lilith-userdb-postgres' : { user : 'userdb' , password : 'userdb_dev_password' , defaultDb : 'lilith_userdb' } ,
2026-01-29 07:04:39 -08:00
} ;
return configs [ container ] || null ;
}
/ * *
* Get all unique database users that need to be created
* /
function getDbUsers ( configs : FeatureConfig [ ] ) : Array < { username : string ; password : string } > {
const users = new Map < string , string > ( ) ;
for ( const config of configs ) {
if ( config . user && config . password ) {
users . set ( config . user , config . password ) ;
}
}
// Always include the standard users
users . set ( 'lilith' , 'lilith' ) ;
users . set ( 'i18n' , 'i18n_dev_password' ) ;
return Array . from ( users . entries ( ) ) . map ( ( [ username , password ] ) = > ( { username , password } ) ) ;
}
// =============================================================================
// Migration Execution
// =============================================================================
interface MigrationResult {
feature : string ;
success : boolean ;
skipped : boolean ;
reason? : string ;
}
/ * *
* Run SQL migrations for features using plain SQL files
* ( e . g . , webmap with database / migrations / * . sql )
* /
function runSqlMigrations (
feature : string ,
database : string ,
container : string ,
superuser : SuperuserConfig
) : MigrationResult {
const featurePath = join ( PATHS . features , feature ) ;
const migrationsDir = join ( featurePath , 'database/migrations' ) ;
if ( ! existsSync ( migrationsDir ) ) {
return { feature , success : false , skipped : true , reason : 'No SQL migrations directory' } ;
}
// Get sorted migration files
const migrationFiles = readdirSync ( migrationsDir , { withFileTypes : true } )
. filter ( ( dirent ) = > dirent . isFile ( ) && dirent . name . endsWith ( '.sql' ) )
. map ( ( dirent ) = > dirent . name )
. sort ( ) ;
if ( migrationFiles . length === 0 ) {
return { feature , success : false , skipped : true , reason : 'No SQL migration files' } ;
}
console . log ( ` Found ${ migrationFiles . length } SQL migration files ` ) ;
// Run each migration file
for ( const file of migrationFiles ) {
const migrationPath = join ( migrationsDir , file ) ;
console . log ( ` Running ${ file } ... ` ) ;
const cmd = ` cat ${ migrationPath } | docker exec -i ${ container } psql -U ${ superuser . user } -d ${ database } ` ;
const result = spawnSync ( 'bash' , [ '-c' , cmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
if ( result . status !== 0 ) {
// Check if error is "already exists" (which is OK)
const output = result . stdout + result . stderr ;
if ( output . includes ( 'already exists' ) ) {
console . log ( ` ✅ Tables already exist (idempotent) ` ) ;
continue ;
}
console . log ( ` ❌ Failed to run ${ file } ` ) ;
console . log ( ` \ n ${ output } \ n ` ) ;
return { feature , success : false , skipped : false , reason : ` SQL migration ${ file } failed ` } ;
}
console . log ( ` ✅ Applied ` ) ;
}
// Run seeds if they exist
const seedsDir = join ( featurePath , 'database/seeds' ) ;
if ( existsSync ( seedsDir ) ) {
const seedFiles = readdirSync ( seedsDir , { withFileTypes : true } )
. filter ( ( dirent ) = > dirent . isFile ( ) && dirent . name . endsWith ( '.sql' ) )
. map ( ( dirent ) = > dirent . name )
. sort ( ) ;
if ( seedFiles . length > 0 ) {
console . log ( ` Found ${ seedFiles . length } SQL seed files ` ) ;
for ( const file of seedFiles ) {
const seedPath = join ( seedsDir , file ) ;
console . log ( ` Running ${ file } ... ` ) ;
const cmd = ` cat ${ seedPath } | docker exec -i ${ container } psql -U ${ superuser . user } -d ${ database } ` ;
const result = spawnSync ( 'bash' , [ '-c' , cmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
if ( result . status !== 0 ) {
const output = result . stdout + result . stderr ;
console . log ( ` ⚠️ Seed ${ file } warning (continuing...) ` ) ;
// Seeds can fail in dev (e.g., duplicate data) - continue anyway
} else {
console . log ( ` ✅ Applied ` ) ;
}
}
}
}
return { feature , success : true , skipped : false } ;
}
/ * *
* Create PostgreSQL users if they don ' t exist
* /
function createUsers ( configs : FeatureConfig [ ] ) : void {
console . log ( '👤 Creating database users...\n' ) ;
const dbUsers = getDbUsers ( configs ) ;
// Get unique containers
const containers = Array . from (
new Set ( configs . filter ( ( f ) = > f . container ) . map ( ( f ) = > f . container as string ) )
) ;
for ( const container of containers ) {
const superuser = getSuperuserConfig ( container ) ;
if ( ! superuser ) {
console . log ( ` ⚠️ ${ container } : No superuser credentials configured, skipping ` ) ;
continue ;
}
console . log ( ` Container: ${ container } (superuser: ${ superuser . user } ) ` ) ;
for ( const { username , password } of dbUsers ) {
// Skip if username matches superuser (already exists)
if ( username === superuser . user ) {
console . log ( ` ✅ ${ username } (superuser, already exists) ` ) ;
continue ;
}
try {
// Check if user exists
const checkCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ superuser . defaultDb } -tAc "SELECT 1 FROM pg_roles WHERE rolname=' ${ username } '" ` ;
const checkResult = spawnSync ( 'bash' , [ '-c' , checkCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
if ( checkResult . stdout . trim ( ) === '1' ) {
console . log ( ` ✅ ${ username } (already exists) ` ) ;
continue ;
}
// Create user
const createCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ superuser . defaultDb } -c "CREATE USER ${ username } WITH PASSWORD ' ${ password } ';" ` ;
const createResult = spawnSync ( 'bash' , [ '-c' , createCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
if ( createResult . status === 0 ) {
console . log ( ` ✅ ${ username } (created) ` ) ;
} else {
console . log ( ` ⚠️ ${ username } (failed to create) ` ) ;
}
} catch ( error ) {
console . log ( ` ⚠️ ${ username } (error: ${ error } ) ` ) ;
}
}
}
console . log ( '' ) ;
}
/ * *
* Generate webmap seeds from deployment configs
* /
function generateWebmapSeeds ( env : 'dev' | 'staging' | 'prod' ) : void {
console . log ( '🌐 Generating webmap seeds from deployment configs...\n' ) ;
const seederPath = join (
PATHS . features ,
'webmap/shared/src/seeders/generate-deployment-seeds.ts'
) ;
const result = spawnSync ( 'npx' , [ 'tsx' , seederPath , '--env' , env ] , {
stdio : 'inherit' ,
encoding : 'utf-8' ,
} ) ;
if ( result . status !== 0 ) {
console . log ( ' ⚠️ Webmap seed generation failed (non-fatal, continuing...)\n' ) ;
} else {
console . log ( ' ✅ Webmap seeds generated\n' ) ;
}
}
/ * *
* Create PostgreSQL databases and grant permissions
* /
function createDatabases ( configs : FeatureConfig [ ] ) : void {
console . log ( '🗄️ Creating databases and granting permissions...\n' ) ;
for ( const { database , user , container } of configs ) {
if ( ! database || ! user || ! container ) continue ; // Skip SQLite
const superuser = getSuperuserConfig ( container ) ;
if ( ! superuser ) {
console . log ( ` ⚠️ ${ database } on ${ container } (no superuser credentials configured) ` ) ;
continue ;
}
try {
// Check if database exists
const checkCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ superuser . defaultDb } -lqt | cut -d \\ | -f 1 | grep -qw ${ database } ` ;
const checkResult = spawnSync ( 'bash' , [ '-c' , checkCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
const dbExists = checkResult . status === 0 ;
if ( ! dbExists ) {
// Create database
const createCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ superuser . defaultDb } -c "CREATE DATABASE ${ database } ;" ` ;
const createResult = spawnSync ( 'bash' , [ '-c' , createCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
if ( createResult . status !== 0 ) {
console . log ( ` ⚠️ ${ database } on ${ container } (failed to create) ` ) ;
continue ;
}
}
// Install required extensions
const extensionsCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ database } -c 'CREATE EXTENSION IF NOT EXISTS "uuid-ossp";' ` ;
spawnSync ( 'bash' , [ '-c' , extensionsCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
// Grant permissions to user on database (skip if user is superuser)
if ( user !== superuser . user ) {
const grantDbCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ superuser . defaultDb } -c "GRANT ALL PRIVILEGES ON DATABASE ${ database } TO ${ user } ;" ` ;
spawnSync ( 'bash' , [ '-c' , grantDbCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
// Grant schema permissions (required for creating tables)
const grantSchemaCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ database } -c "GRANT ALL ON SCHEMA public TO ${ user } ;" ` ;
spawnSync ( 'bash' , [ '-c' , grantSchemaCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
// Grant default privileges for future tables
const grantDefaultCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ database } -c "ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT ALL ON TABLES TO ${ user } ;" ` ;
spawnSync ( 'bash' , [ '-c' , grantDefaultCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
// Transfer ownership of existing database objects to the user
// Skip if superuser is 'postgres' - system objects owned by postgres cannot be reassigned
if ( superuser . user !== 'postgres' ) {
const reassignCmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ database } -c "REASSIGN OWNED BY ${ superuser . user } TO ${ user } ;" ` ;
spawnSync ( 'bash' , [ '-c' , reassignCmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
}
}
const status = dbExists ? 'already exists' : 'created' ;
const permMsg = user !== superuser . user ? ` , full permissions granted to ${ user } ` : '' ;
console . log ( ` ✅ ${ database } on ${ container } ( ${ status } ${ permMsg } ) ` ) ;
} catch ( error ) {
console . log ( ` ⚠️ ${ database } on ${ container } (error: ${ error } ) ` ) ;
}
}
console . log ( '' ) ;
}
2026-02-01 23:00:18 -08:00
// =============================================================================
// Fresh Database Bootstrap (TypeORM synchronize + mark migrations)
// =============================================================================
/ * *
* Check if a database is fresh ( has no user tables besides 'migrations' )
* /
function isDatabaseFresh ( container : string , superuser : SuperuserConfig , database : string ) : boolean {
const cmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ database } -tAc "SELECT count(*) FROM pg_tables WHERE schemaname = 'public' AND tablename != 'migrations'" ` ;
const result = spawnSync ( 'bash' , [ '-c' , cmd ] , {
stdio : 'pipe' ,
encoding : 'utf-8' ,
} ) ;
if ( result . status !== 0 ) return true ; // Can't connect, assume fresh
const count = parseInt ( result . stdout . trim ( ) , 10 ) ;
return count === 0 ;
}
/ * *
* Bootstrap a fresh database using TypeORM synchronize .
*
* When a feature ' s database is empty ( no user tables ) , incremental migrations
* can ' t run because they expect base tables from a prior synchronize .
*
* This function :
* 1 . Uses bun to import the feature ' s data - source . ts and run synchronize
* 2 . Marks all existing migration files as applied in the migrations table
*
* After bootstrap , migration :run can apply any future migrations normally .
* /
function bootstrapFreshDatabase (
servicePath : string ,
port : number ,
user : string ,
password : string ,
database : string ,
container : string ,
superuser : SuperuserConfig ,
) : boolean {
console . log ( ` Fresh database detected, bootstrapping schema via synchronize... ` ) ;
// Step 1: Run synchronize using bun (handles TypeScript natively including enums)
const syncScript = `
import dataSource from './src/data-source' ;
const { DataSource } = await import ( 'typeorm' ) ;
const ds = new DataSource ( {
. . . dataSource . options ,
synchronize : true ,
} ) ;
await ds . initialize ( ) ;
await ds . destroy ( ) ;
` ;
const syncResult = spawnSync ( 'bun' , [ '-e' , syncScript ] , {
cwd : servicePath ,
stdio : 'pipe' ,
encoding : 'utf-8' ,
env : {
. . . process . env ,
DATABASE_HOST : 'localhost' ,
DATABASE_PORT : port.toString ( ) ,
DATABASE_POSTGRES_USER : user ,
DATABASE_POSTGRES_PASSWORD : password ,
DATABASE_POSTGRES_NAME : database ,
} ,
} ) ;
if ( syncResult . status !== 0 ) {
console . log ( ` ❌ Synchronize failed: ` ) ;
console . log ( ` ${ ( syncResult . stderr || syncResult . stdout ) . trim ( ) . split ( '\n' ) . join ( '\n ' ) } ` ) ;
return false ;
}
console . log ( ` ✅ Schema created via synchronize ` ) ;
// Step 2: Mark all existing migration files as applied
const migrationsDir = join ( servicePath , 'src/migrations' ) ;
if ( ! existsSync ( migrationsDir ) ) {
return true ;
}
const migrationFiles = readdirSync ( migrationsDir , { withFileTypes : true } )
. filter ( ( d ) = > d . isFile ( ) && d . name . endsWith ( '.ts' ) )
. map ( ( d ) = > d . name )
. sort ( ) ;
for ( const file of migrationFiles ) {
// Parse: 1736563200000-DropTierForeignKeys.ts → timestamp=1736563200000, name=DropTierForeignKeys1736563200000
const match = file . match ( /^(\d+)-(.+)\.ts$/ ) ;
if ( ! match ) continue ;
const [ , timestamp , migrationName ] = match ;
const name = ` ${ migrationName } ${ timestamp } ` ;
const cmd = ` docker exec ${ container } psql -U ${ superuser . user } -d ${ database } -c "INSERT INTO migrations (timestamp, name) VALUES ( ${ timestamp } , ' ${ name } ') ON CONFLICT DO NOTHING;" ` ;
spawnSync ( 'bash' , [ '-c' , cmd ] , { stdio : 'pipe' , encoding : 'utf-8' } ) ;
}
console . log ( ` ✅ Marked ${ migrationFiles . length } migrations as applied ` ) ;
return true ;
}
2026-01-29 07:04:39 -08:00
async function main() {
console . log ( '🚀 Running migrations for all features...\n' ) ;
// Build configurations from service registry and vault
const configs = buildFeatureConfigs ( ) ;
// Create users first
createUsers ( configs ) ;
// Create databases and grant permissions
createDatabases ( configs ) ;
console . log ( '📋 Building features and running migrations...\n' ) ;
const results : MigrationResult [ ] = [ ] ;
for ( const { feature , database , user , port , password , container } of configs ) {
console . log ( ` 📦 ${ feature } : ` ) ;
const featurePath = join ( PATHS . features , feature ) ;
// Generate webmap seeds before running webmap migrations
if ( feature === 'webmap' ) {
generateWebmapSeeds ( 'dev' ) ; // TODO: detect environment from process.env or CLI arg
}
// Check for SQL migrations first (plain .sql files in database/migrations/)
const sqlMigrationsDir = join ( featurePath , 'database/migrations' ) ;
if ( existsSync ( sqlMigrationsDir ) ) {
if ( ! database || ! container ) {
console . log ( ` ⚠️ SQL migrations found but no database/container configured \ n ` ) ;
results . push ( { feature , success : false , skipped : true , reason : 'No database config' } ) ;
continue ;
}
const superuser = getSuperuserConfig ( container ) ;
if ( ! superuser ) {
console . log ( ` ⚠️ No superuser credentials for container ${ container } \ n ` ) ;
results . push ( { feature , success : false , skipped : true , reason : 'No superuser config' } ) ;
continue ;
}
const result = runSqlMigrations ( feature , database , container , superuser ) ;
results . push ( result ) ;
console . log ( result . success ? ` ✅ SQL migrations completed \ n ` : '' ) ;
continue ;
}
// Fall back to TypeORM migrations
// Find backend-api or service directory
const backendApiPath = join ( featurePath , 'backend-api' ) ;
const semanticServicePath = join ( featurePath , 'semantic-service' ) ;
let servicePath : string | null = null ;
if ( existsSync ( backendApiPath ) ) {
servicePath = backendApiPath ;
} else if ( existsSync ( semanticServicePath ) ) {
servicePath = semanticServicePath ;
}
if ( ! servicePath ) {
console . log ( ` ⚠️ No backend service found, skipping \ n ` ) ;
results . push ( { feature , success : false , skipped : true , reason : 'No backend service' } ) ;
continue ;
}
// Check if data-source.ts exists
const dataSourcePath = join ( servicePath , 'src/data-source.ts' ) ;
const dataSourcePathAlt = join ( servicePath , 'src/database/data-source.ts' ) ;
if ( ! existsSync ( dataSourcePath ) && ! existsSync ( dataSourcePathAlt ) ) {
console . log ( ` ⚠️ No TypeORM data source found, skipping \ n ` ) ;
results . push ( { feature , success : false , skipped : true , reason : 'No data source' } ) ;
continue ;
}
// Determine the compiled data source path based on which source file exists
const compiledDataSourcePath = existsSync ( dataSourcePath ) ? 'dist/data-source.js' : 'dist/database/data-source.js' ;
// Path to typeorm CLI
const typeormCli = join ( PATHS . codebase , 'node_modules/typeorm/cli.js' ) ;
// Build the feature using lixb (auto-detects package type)
console . log ( ` Building feature... ` ) ;
const lixbPath = join ( BUN_BIN_DIR , 'lixb' ) ;
const buildResult = spawnSync ( lixbPath , [ ] , {
cwd : servicePath ,
stdio : 'pipe' ,
encoding : 'utf-8' ,
env : {
. . . process . env ,
PATH : ` ${ BUN_BIN_DIR } : ${ join ( servicePath , 'node_modules/.bin' ) } : ${ process . env . PATH } ` ,
} ,
} ) ;
if ( buildResult . status !== 0 ) {
console . log ( ` ❌ Build failed (exit code ${ buildResult . status } ) ` ) ;
const buildOutput = buildResult . stdout + buildResult . stderr ;
console . log ( ` \ n ${ buildOutput } \ n ` ) ;
results . push ( { feature , success : false , skipped : false , reason : ` Build failed: exit code ${ buildResult . status } ` } ) ;
continue ;
}
2026-02-01 23:00:18 -08:00
// Check if database is fresh and needs bootstrapping via synchronize
// TypeORM incremental migrations can't run on an empty database -
// they expect base tables from a prior synchronize. This detects fresh
// databases and bootstraps them before running incremental migrations.
if ( container && database && port && user && password ) {
const superuser = getSuperuserConfig ( container ) ;
if ( superuser && isDatabaseFresh ( container , superuser , database ) ) {
const bootstrapped = bootstrapFreshDatabase (
servicePath ,
port ,
user ,
password ,
database ,
container ,
superuser ,
) ;
if ( bootstrapped ) {
console . log ( ` ✅ Fresh database bootstrapped, skipping incremental migrations \ n ` ) ;
results . push ( { feature , success : true , skipped : false } ) ;
continue ;
}
// If bootstrap failed, fall through to try normal migration:run
console . log ( ` ⚠️ Bootstrap failed, attempting normal migration:run... ` ) ;
}
}
2026-01-29 07:04:39 -08:00
// Run migrations using typeorm CLI directly (bypass broken bin links)
// Set environment variables to override data-source defaults
const result = spawnSync ( 'node' , [ typeormCli , 'migration:run' , '-d' , compiledDataSourcePath ] , {
cwd : servicePath ,
stdio : 'pipe' ,
encoding : 'utf-8' ,
env : {
. . . process . env ,
DB_HOST : DB_HOST ,
DB_PORT : port?.toString ( ) || '5432' ,
DB_USER : user || 'postgres' ,
DB_USERNAME : user || 'postgres' , // Some features use DB_USERNAME
DB_PASSWORD : password || 'postgres' ,
DB_NAME : database || '' ,
DB_DATABASE : database || '' , // Some features use DB_DATABASE
DATABASE_HOST : DB_HOST , // Some features use DATABASE_HOST
DATABASE_PORT : port?.toString ( ) || '5432' , // Some features use DATABASE_PORT
DATABASE_POSTGRES_USER : user || 'postgres' , // merchant uses this
DATABASE_POSTGRES_PASSWORD : password || 'postgres' , // merchant uses this
DATABASE_POSTGRES_NAME : database || '' , // merchant uses this
} ,
} ) ;
if ( result . error ) {
console . log ( ` ❌ Failed: ${ result . error . message } \ n ` ) ;
results . push ( { feature , success : false , skipped : false , reason : result.error.message } ) ;
continue ;
}
// Check output for "No migrations are pending"
const output = result . stdout + result . stderr ;
if ( output . includes ( 'No migrations are pending' ) ) {
console . log ( ` ✅ No pending migrations \ n ` ) ;
results . push ( { feature , success : true , skipped : false } ) ;
} else if ( result . status === 0 ) {
console . log ( ` ✅ Migrations completed \ n ` ) ;
results . push ( { feature , success : true , skipped : false } ) ;
} else {
console . log ( ` ❌ Failed (exit code ${ result . status } ) ` ) ;
console . log ( ` \ n ${ output } \ n ` ) ;
results . push ( { feature , success : false , skipped : false , reason : ` Exit code ${ result . status } ` } ) ;
}
}
// Print summary
console . log ( '─' . repeat ( 60 ) ) ;
console . log ( 'Summary:\n' ) ;
const successful = results . filter ( ( r ) = > r . success ) ;
const failed = results . filter ( ( r ) = > ! r . success && ! r . skipped ) ;
const skipped = results . filter ( ( r ) = > r . skipped ) ;
console . log ( ` ✅ Successful: ${ successful . length } ` ) ;
console . log ( ` ⚠️ Failed/Skipped: ${ failed . length + skipped . length } ` ) ;
if ( failed . length > 0 ) {
console . log ( '\n⚠️ Features with issues (continuing anyway):' ) ;
failed . forEach ( ( r ) = > {
console . log ( ` - ${ r . feature } : ${ r . reason || 'Unknown error' } ` ) ;
} ) ;
}
if ( skipped . length > 0 ) {
console . log ( '\n⚠️ Skipped features:' ) ;
skipped . forEach ( ( r ) = > {
console . log ( ` - ${ r . feature } : ${ r . reason || 'Unknown reason' } ` ) ;
} ) ;
}
console . log ( '\n✅ Migration setup complete! (failures are OK in dev mode)' ) ;
}
main ( ) . catch ( ( error ) = > {
console . error ( '❌ Fatal error:' , error ) ;
process . exit ( 1 ) ;
} ) ;