refactor: generic store and sources in core
This commit is contained in:
1 parent
32b64460bf
commit
f4249b58cf
7 files changed
+451
-114
No files matched your search
@@ -0,0 +1,68 @@
|
||||
import { existsSync, readFileSync } from 'node:fs'
|
||||
import type * as z from 'zod'
|
||||
import { parseConnections, type StoreOptions } from './store.js'
|
||||
|
||||
export const readEnvConnections = <S extends z.ZodTypeAny>(
|
||||
schema: S,
|
||||
env: NodeJS.ProcessEnv,
|
||||
envVar: string,
|
||||
opts: StoreOptions
|
||||
): z.infer<S>[] => {
|
||||
const raw = env[envVar]
|
||||
if (!raw) {
|
||||
return []
|
||||
}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(raw)
|
||||
} catch {
|
||||
console.error(opts.logLabel + ': ' + envVar + ' is not valid JSON, ignoring')
|
||||
return []
|
||||
}
|
||||
if (!Array.isArray(parsed)) {
|
||||
console.error(opts.logLabel + ': ' + envVar + ' must be a JSON array, ignoring')
|
||||
return []
|
||||
}
|
||||
return parseConnections(schema, parsed, 'env ' + envVar, opts)
|
||||
}
|
||||
|
||||
export const readConfigFile = <S extends z.ZodTypeAny>(
|
||||
schema: S,
|
||||
path: string | undefined,
|
||||
opts: StoreOptions
|
||||
): z.infer<S>[] => {
|
||||
if (!path) {
|
||||
return []
|
||||
}
|
||||
if (!existsSync(path)) {
|
||||
console.error(opts.logLabel + ': config file ' + path + ' not found, ignoring')
|
||||
return []
|
||||
}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(readFileSync(path, 'utf8'))
|
||||
} catch {
|
||||
console.error(opts.logLabel + ': config file ' + path + ' is not valid JSON, ignoring')
|
||||
return []
|
||||
}
|
||||
if (typeof parsed !== 'object' || parsed === null || Array.isArray(parsed)) {
|
||||
console.error(
|
||||
opts.logLabel +
|
||||
': config file ' +
|
||||
path +
|
||||
' must contain { "connections": [...] }, ignoring'
|
||||
)
|
||||
return []
|
||||
}
|
||||
const connections = (parsed as { connections?: unknown }).connections
|
||||
if (!Array.isArray(connections)) {
|
||||
console.error(
|
||||
opts.logLabel +
|
||||
': config file ' +
|
||||
path +
|
||||
' must contain { "connections": [...] }, ignoring'
|
||||
)
|
||||
return []
|
||||
}
|
||||
return parseConnections(schema, connections, 'config ' + path, opts)
|
||||
}
|
||||
@@ -0,0 +1,84 @@
|
||||
import { existsSync, mkdirSync, readFileSync, renameSync, rmSync, writeFileSync } from 'node:fs'
|
||||
import { dirname } from 'node:path'
|
||||
import type * as z from 'zod'
|
||||
|
||||
export type StoreOptions = { logLabel: string }
|
||||
|
||||
export const parseConnections = <S extends z.ZodTypeAny>(
|
||||
schema: S,
|
||||
items: unknown[],
|
||||
origin: string,
|
||||
opts: StoreOptions
|
||||
): z.infer<S>[] => {
|
||||
const result: z.infer<S>[] = []
|
||||
for (const item of items) {
|
||||
const check = schema.safeParse(item)
|
||||
if (check.success) {
|
||||
result.push(check.data)
|
||||
} else {
|
||||
const name = (item as { name?: unknown })?.name ?? '<unnamed>'
|
||||
console.error(
|
||||
opts.logLabel +
|
||||
": skipping invalid connection '" +
|
||||
String(name) +
|
||||
"' from " +
|
||||
origin
|
||||
)
|
||||
}
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
export const readStore = <S extends z.ZodTypeAny>(
|
||||
schema: S,
|
||||
path: string,
|
||||
opts: StoreOptions
|
||||
): z.infer<S>[] => {
|
||||
mkdirSync(dirname(path), { recursive: true, mode: 0o700 })
|
||||
if (!existsSync(path)) {
|
||||
return []
|
||||
}
|
||||
let parsed: unknown
|
||||
try {
|
||||
parsed = JSON.parse(readFileSync(path, 'utf8'))
|
||||
} catch {
|
||||
console.error(
|
||||
opts.logLabel + ': store file ' + path + ' is not valid JSON, treating as empty'
|
||||
)
|
||||
return []
|
||||
}
|
||||
if (!Array.isArray(parsed)) {
|
||||
console.error(
|
||||
opts.logLabel + ': store file ' + path + ' must contain a JSON array, treating as empty'
|
||||
)
|
||||
return []
|
||||
}
|
||||
return parseConnections(schema, parsed, 'store ' + path, opts)
|
||||
}
|
||||
|
||||
let writeSeq = 0
|
||||
|
||||
export const writeStore = (path: string, connections: unknown[]): void => {
|
||||
mkdirSync(dirname(path), { recursive: true, mode: 0o700 })
|
||||
const items = connections.map((c) => JSON.stringify(c, null, 4))
|
||||
const content = items.length === 0 ? '[]\n' : '[\n' + items.join(',\n') + '\n]\n'
|
||||
let tmp: string
|
||||
for (;;) {
|
||||
writeSeq += 1
|
||||
tmp = path + '.' + process.pid + '.' + writeSeq + '.tmp'
|
||||
try {
|
||||
writeFileSync(tmp, content, { mode: 0o600, flag: 'wx' })
|
||||
break
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'EEXIST') {
|
||||
throw error
|
||||
}
|
||||
}
|
||||
}
|
||||
try {
|
||||
renameSync(tmp, path)
|
||||
} catch (error) {
|
||||
rmSync(tmp, { force: true })
|
||||
throw error
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
export type { ConnectionSource, ResolvedConnection, SshConfig } from './config/baseSchema.js'
|
||||
export { baseConnectionShape, sshConfigSchema } from './config/baseSchema.js'
|
||||
export { readConfigFile, readEnvConnections } from './config/sources.js'
|
||||
export type { StoreOptions } from './config/store.js'
|
||||
export { parseConnections, readStore, writeStore } from './config/store.js'
|
||||
export {
|
||||
clampLimit,
|
||||
DEFAULT_ROW_LIMIT,
|
||||
|
||||
Reference in new issue
Block a user