feat: add persistent store, atomic 0600 writes

This commit is contained in:
smartass
2026-06-11 23:15:05 +05:00
parent 1e007207a3
commit 87b3f118ed
2 changed files with 141 additions and 0 deletions
+91
View File
@@ -0,0 +1,91 @@
import {
mkdirSync,
mkdtempSync,
readdirSync,
readFileSync,
rmSync,
statSync,
writeFileSync
} from 'node:fs'
import { tmpdir } from 'node:os'
import { join } from 'node:path'
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'
import { defaultStorePath, readStore, writeStore } from '../../../src/config/store.js'
import type { ConnectionConfig } from '../../../src/config/types.js'
const connection: ConnectionConfig = {
name: 'lab',
type: 'postgres',
host: 'localhost',
user: 'postgres',
readonly: false
}
describe('store', () => {
let dir: string
let path: string
beforeEach(() => {
dir = mkdtempSync(join(tmpdir(), 'dbmole-store-'))
path = join(dir, 'nested', 'connections.json')
vi.spyOn(console, 'error').mockImplementation(() => {})
})
afterEach(() => {
rmSync(dir, { recursive: true, force: true })
vi.restoreAllMocks()
})
it('reads missing file as empty list', () => {
expect(readStore(path)).toEqual([])
})
it('round-trips connections and creates parent dirs', () => {
writeStore(path, [connection])
expect(readStore(path)).toEqual([connection])
})
it('writes file with 0600 permissions and leaves no tmp files', () => {
writeStore(path, [connection])
const mode = statSync(path).mode & 0o777
expect(mode).toBe(0o600)
const leftovers = readdirSync(join(dir, 'nested')).filter((f) => f.includes('.tmp'))
expect(leftovers).toEqual([])
})
it('treats invalid JSON as empty with a warning', () => {
writeStore(path, [connection])
writeFileSync(path, '{ broken')
expect(readStore(path)).toEqual([])
expect(console.error).toHaveBeenCalled()
})
it('treats non-array JSON as empty with a warning', () => {
writeStore(path, [connection])
writeFileSync(path, '{"connections": []}')
expect(readStore(path)).toEqual([])
expect(console.error).toHaveBeenCalled()
})
it('skips invalid entries but keeps valid ones', () => {
mkdirSync(join(dir, 'nested'), { recursive: true })
writeFileSync(path, JSON.stringify([connection, { name: 'broken' }]))
expect(readStore(path)).toEqual([connection])
expect(console.error).toHaveBeenCalled()
})
it('stores pretty JSON for hand editing', () => {
writeStore(path, [connection])
expect(readFileSync(path, 'utf8')).toContain('\n "name": "lab"')
})
})
describe('defaultStorePath', () => {
it('honors DBMOLE_STORE override', () => {
expect(defaultStorePath({ DBMOLE_STORE: '/tmp/custom.json' })).toBe('/tmp/custom.json')
})
it('falls back to ~/.config/dbmole/connections.json', () => {
expect(defaultStorePath({})).toMatch(/\.config\/dbmole\/connections\.json$/)
})
})