From 46906e3f5b57aebccd04d99a674f9ded0bb7293c Mon Sep 17 00:00:00 2001 From: smartass Date: Thu, 11 Jun 2026 23:20:33 +0500 Subject: [PATCH] feat: add env and config-file sources --- src/config/sources.ts | 47 +++++++++++++++++++++ test/unit/config/sources.test.ts | 72 ++++++++++++++++++++++++++++++++ 2 files changed, 119 insertions(+) create mode 100644 src/config/sources.ts create mode 100644 test/unit/config/sources.test.ts diff --git a/src/config/sources.ts b/src/config/sources.ts new file mode 100644 index 0000000..d778f4b --- /dev/null +++ b/src/config/sources.ts @@ -0,0 +1,47 @@ +import { existsSync, readFileSync } from 'node:fs' +import { parseConnections } from './store.js' +import type { ConnectionConfig } from './types.js' + +export const readEnvConnections = (env: NodeJS.ProcessEnv = process.env): ConnectionConfig[] => { + const raw = env.DBMOLE_CONNECTIONS + if (!raw) { + return [] + } + let parsed: unknown + try { + parsed = JSON.parse(raw) + } catch { + console.error('dbmole: DBMOLE_CONNECTIONS is not valid JSON, ignoring') + return [] + } + if (!Array.isArray(parsed)) { + console.error('dbmole: DBMOLE_CONNECTIONS must be a JSON array, ignoring') + return [] + } + return parseConnections(parsed, 'env DBMOLE_CONNECTIONS') +} + +export const readConfigFile = (path: string | undefined): ConnectionConfig[] => { + if (!path) { + return [] + } + if (!existsSync(path)) { + console.error('dbmole: config file ' + path + ' not found, ignoring') + return [] + } + let parsed: unknown + try { + parsed = JSON.parse(readFileSync(path, 'utf8')) + } catch { + console.error('dbmole: config file ' + path + ' is not valid JSON, ignoring') + return [] + } + const connections = (parsed as { connections?: unknown }).connections + if (!Array.isArray(connections)) { + console.error( + 'dbmole: config file ' + path + ' must contain { "connections": [...] }, ignoring' + ) + return [] + } + return parseConnections(connections, 'config ' + path) +} diff --git a/test/unit/config/sources.test.ts b/test/unit/config/sources.test.ts new file mode 100644 index 0000000..8be6ba5 --- /dev/null +++ b/test/unit/config/sources.test.ts @@ -0,0 +1,72 @@ +import { mkdtempSync, rmSync, writeFileSync } from 'node:fs' +import { tmpdir } from 'node:os' +import { join } from 'node:path' +import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest' +import { readConfigFile, readEnvConnections } from '../../../src/config/sources.js' + +const valid = { name: 'env-pg', type: 'postgres', host: 'h', user: 'u' } + +describe('readEnvConnections', () => { + beforeEach(() => { + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + afterEach(() => { + vi.restoreAllMocks() + }) + + it('returns empty when unset', () => { + expect(readEnvConnections({})).toEqual([]) + }) + + it('parses a JSON array of connections', () => { + const env = { DBMOLE_CONNECTIONS: JSON.stringify([valid]) } + expect(readEnvConnections(env).map((c) => c.name)).toEqual(['env-pg']) + }) + + it('ignores invalid JSON with a warning', () => { + expect(readEnvConnections({ DBMOLE_CONNECTIONS: 'nope{' })).toEqual([]) + expect(console.error).toHaveBeenCalled() + }) + + it('ignores non-array JSON with a warning', () => { + expect(readEnvConnections({ DBMOLE_CONNECTIONS: '{}' })).toEqual([]) + expect(console.error).toHaveBeenCalled() + }) +}) + +describe('readConfigFile', () => { + let dir: string + + beforeEach(() => { + dir = mkdtempSync(join(tmpdir(), 'dbmole-config-')) + vi.spyOn(console, 'error').mockImplementation(() => {}) + }) + + afterEach(() => { + rmSync(dir, { recursive: true, force: true }) + vi.restoreAllMocks() + }) + + it('returns empty when path is undefined', () => { + expect(readConfigFile(undefined)).toEqual([]) + }) + + it('warns and returns empty for a missing file', () => { + expect(readConfigFile(join(dir, 'absent.json'))).toEqual([]) + expect(console.error).toHaveBeenCalled() + }) + + it('reads { connections: [...] } shape', () => { + const path = join(dir, 'config.json') + writeFileSync(path, JSON.stringify({ connections: [valid] })) + expect(readConfigFile(path).map((c) => c.name)).toEqual(['env-pg']) + }) + + it('warns on wrong shape', () => { + const path = join(dir, 'config.json') + writeFileSync(path, JSON.stringify([valid])) + expect(readConfigFile(path)).toEqual([]) + expect(console.error).toHaveBeenCalled() + }) +})