feat(xo-server-db): minimal CLI to browser the DB
This commit is contained in:
@@ -20,6 +20,7 @@
|
||||
"preferGlobal": true,
|
||||
"bin": {
|
||||
"xo-server": "dist/cli.mjs",
|
||||
"xo-server-db": "dist/db-cli.mjs",
|
||||
"xo-server-logs": "dist/logs-cli.mjs",
|
||||
"xo-server-recover-account": "dist/recover-account-cli.mjs"
|
||||
},
|
||||
|
||||
117
packages/xo-server/src/db-cli.mjs
Executable file
117
packages/xo-server/src/db-cli.mjs
Executable file
@@ -0,0 +1,117 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
import { createClient as createRedisClient } from 'redis'
|
||||
import appConf from 'app-conf'
|
||||
import fromCallback from 'promise-toolbox/fromCallback'
|
||||
import fromEvent from 'promise-toolbox/fromEvent'
|
||||
|
||||
import RedisCollection from './collection/redis.mjs'
|
||||
|
||||
function assert(test, message) {
|
||||
if (!test) {
|
||||
console.error(message)
|
||||
process.exit(1)
|
||||
}
|
||||
}
|
||||
|
||||
async function getDb(namespace) {
|
||||
const { connection } = this
|
||||
return new RedisCollection({
|
||||
connection,
|
||||
indexes: await fromCallback.call(connection, 'smembers', `xo:${namespace}::indexes`),
|
||||
namespace,
|
||||
})
|
||||
}
|
||||
|
||||
function parseParam(args) {
|
||||
const params = {}
|
||||
for (const arg of args) {
|
||||
const i = arg.indexOf('=')
|
||||
if (i === -1) {
|
||||
throw new Error('invalid arg: ' + arg)
|
||||
}
|
||||
|
||||
const name = arg.slice(0, i)
|
||||
let value = arg.slice(i + 1)
|
||||
if (value.startsWith('json:')) {
|
||||
value = JSON.parse(value.slice(5))
|
||||
}
|
||||
params[name] = value
|
||||
}
|
||||
return params
|
||||
}
|
||||
|
||||
function sortKeys(object) {
|
||||
const result = {}
|
||||
const keys = Object.keys(object).sort()
|
||||
for (const key of keys) {
|
||||
result[key] = object[key]
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
const COMMANDS = {
|
||||
async ls(args) {
|
||||
if (args.length === 0) {
|
||||
const namespaces = await fromCallback.call(this.connection, 'smembers', 'xo::namespaces')
|
||||
namespaces.sort()
|
||||
for (const ns of namespaces) {
|
||||
console.log(ns)
|
||||
}
|
||||
} else {
|
||||
const db = await this.getDb(args.shift())
|
||||
const records = await db.get(parseParam(args))
|
||||
process.stdout.write(`${records.length} record(s) found\n`)
|
||||
for (const record of records) {
|
||||
console.log(sortKeys(record))
|
||||
}
|
||||
}
|
||||
},
|
||||
}
|
||||
|
||||
async function main(args) {
|
||||
if (args.length === 0 || args.includes('-h') || args.includes('--help')) {
|
||||
process.stdout.write(`
|
||||
xo-server-db --help, -h
|
||||
|
||||
Display this help message.
|
||||
|
||||
xo-server-logs ls
|
||||
|
||||
List the available namespaces.
|
||||
|
||||
xo-server-logs ls <namespace> [<pattern>...]
|
||||
|
||||
List all entries in the given namespace.
|
||||
|
||||
<pattern>
|
||||
Patterns can be used to filter entries.
|
||||
|
||||
Patterns have the following format \`<field>=<value>\`.
|
||||
|
||||
`)
|
||||
return
|
||||
}
|
||||
|
||||
const config = await appConf.load('xo-server', {
|
||||
appDir: new URL('..', import.meta.url).pathname,
|
||||
ignoreUnknownFormats: true,
|
||||
})
|
||||
|
||||
const { renameCommands, socket: path, uri: url } = config.redis || {}
|
||||
const connection = createRedisClient({
|
||||
path,
|
||||
rename_commands: renameCommands,
|
||||
url,
|
||||
})
|
||||
try {
|
||||
const fn = COMMANDS[args.shift()]
|
||||
assert(fn !== undefined, 'command must be one of: ' + Object.keys(COMMANDS).join(', '))
|
||||
|
||||
await fn.call({ connection, getDb }, args)
|
||||
} finally {
|
||||
connection.quit()
|
||||
await fromEvent(connection, 'end')
|
||||
}
|
||||
}
|
||||
main(process.argv.slice(2))
|
||||
Reference in New Issue
Block a user