import { readFileSync, existsSync } from 'node:fs'; import { resolve } from 'node:path'; import type { StorageConfig } from '@mosaicstack/storage'; import type { QueueAdapterConfig as QueueConfig } from '@mosaicstack/queue'; /* ------------------------------------------------------------------ */ /* Types */ /* ------------------------------------------------------------------ */ export type StorageTier = 'local' | 'standalone' | 'federated'; export interface MemoryConfigRef { type: 'pgvector' | 'sqlite-vec' | 'keyword'; } export interface MosaicConfig { tier: StorageTier; storage: StorageConfig; queue: QueueConfig; memory: MemoryConfigRef; } /* ------------------------------------------------------------------ */ /* Defaults */ /* ------------------------------------------------------------------ */ export const DEFAULT_LOCAL_CONFIG: MosaicConfig = { tier: 'local', storage: { type: 'pglite', dataDir: '.mosaic/storage-pglite' }, queue: { type: 'local', dataDir: '.mosaic/queue' }, memory: { type: 'keyword' }, }; export const DEFAULT_STANDALONE_CONFIG: MosaicConfig = { tier: 'standalone', storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5432/mosaic' }, queue: { type: 'bullmq' }, memory: { type: 'keyword' }, }; export const DEFAULT_FEDERATED_CONFIG: MosaicConfig = { tier: 'federated', storage: { type: 'postgres', url: 'postgresql://mosaic:mosaic@localhost:5433/mosaic', enableVector: true, }, queue: { type: 'bullmq' }, memory: { type: 'pgvector' }, }; /* ------------------------------------------------------------------ */ /* Validation */ /* ------------------------------------------------------------------ */ const VALID_TIERS = new Set(['local', 'standalone', 'federated']); const VALID_STORAGE_TYPES = new Set(['postgres', 'pglite', 'files']); const VALID_QUEUE_TYPES = new Set(['bullmq', 'local']); const VALID_MEMORY_TYPES = new Set(['pgvector', 'sqlite-vec', 'keyword']); export function validateConfig(raw: unknown): MosaicConfig { if (typeof raw !== 'object' || raw === null) { throw new Error('MosaicConfig must be a non-null object'); } const obj = raw as Record; // tier let tier = obj['tier']; // Deprecated alias: 'team' → 'standalone' (kept for backward-compat with 0.0.x installs) if (tier === 'team') { process.stderr.write( '[mosaic] DEPRECATED: tier="team" is deprecated — use "standalone" instead. ' + 'Update your mosaic.config.json.\n', ); tier = 'standalone'; } if (typeof tier !== 'string' || !VALID_TIERS.has(tier)) { throw new Error( `Invalid tier "${String(tier)}" — expected "local", "standalone", or "federated"`, ); } // storage const storage = obj['storage']; if (typeof storage !== 'object' || storage === null) { throw new Error('config.storage must be a non-null object'); } const storageType = (storage as Record)['type']; if (typeof storageType !== 'string' || !VALID_STORAGE_TYPES.has(storageType)) { throw new Error(`Invalid storage.type "${String(storageType)}"`); } // queue const queue = obj['queue']; if (typeof queue !== 'object' || queue === null) { throw new Error('config.queue must be a non-null object'); } const queueType = (queue as Record)['type']; if (typeof queueType !== 'string' || !VALID_QUEUE_TYPES.has(queueType)) { throw new Error(`Invalid queue.type "${String(queueType)}"`); } // memory const memory = obj['memory']; if (typeof memory !== 'object' || memory === null) { throw new Error('config.memory must be a non-null object'); } const memoryType = (memory as Record)['type']; if (typeof memoryType !== 'string' || !VALID_MEMORY_TYPES.has(memoryType)) { throw new Error(`Invalid memory.type "${String(memoryType)}"`); } return { tier: tier as StorageTier, storage: storage as StorageConfig, queue: queue as QueueConfig, memory: memory as MemoryConfigRef, }; } /* ------------------------------------------------------------------ */ /* Loader */ /* ------------------------------------------------------------------ */ export function detectFromEnv(): MosaicConfig { const tier = process.env['MOSAIC_STORAGE_TIER']; if (tier === 'federated') { if (process.env['DATABASE_URL']) { return { ...DEFAULT_FEDERATED_CONFIG, storage: { type: 'postgres', url: process.env['DATABASE_URL'], enableVector: true, }, queue: { type: 'bullmq', url: process.env['VALKEY_URL'], }, }; } // MOSAIC_STORAGE_TIER=federated without DATABASE_URL — use the default // federated config (port 5433, enableVector: true, pgvector memory). return DEFAULT_FEDERATED_CONFIG; } if (tier === 'standalone') { if (process.env['DATABASE_URL']) { return { ...DEFAULT_STANDALONE_CONFIG, storage: { type: 'postgres', url: process.env['DATABASE_URL'], }, queue: { type: 'bullmq', url: process.env['VALKEY_URL'], }, }; } // MOSAIC_STORAGE_TIER=standalone without DATABASE_URL — use the default // standalone config instead of silently falling back to local. return DEFAULT_STANDALONE_CONFIG; } // Legacy: DATABASE_URL set without MOSAIC_STORAGE_TIER — treat as standalone. if (process.env['DATABASE_URL']) { return { ...DEFAULT_STANDALONE_CONFIG, storage: { type: 'postgres', url: process.env['DATABASE_URL'], }, queue: { type: 'bullmq', url: process.env['VALKEY_URL'], }, }; } return DEFAULT_LOCAL_CONFIG; } export function loadConfig(configPath?: string): MosaicConfig { // 1. Explicit path or default location const paths = configPath ? [resolve(configPath)] : [ resolve(process.cwd(), 'mosaic.config.json'), resolve(process.cwd(), '../../mosaic.config.json'), // monorepo root when cwd is apps/gateway ]; for (const p of paths) { if (existsSync(p)) { const raw: unknown = JSON.parse(readFileSync(p, 'utf-8')); return validateConfig(raw); } } // 2. Fall back to env-var detection return detectFromEnv(); }