#!/usr/bin/env node import { argv } from 'node:process' import yargs from 'yargs' import { getXmlRpcClient } from '../../index.js' import { envOptions as connectionOptions } from '../connection.js' // the more complex a query gets that is executed // the more sense it makes to read it from a file // see db.app.install and how it uses xquery/install-package.xq const query = `xquery version "3.1"; declare variable $collection external; declare variable $level external; declare function local:get-descendant-resources ($collection, $current-level) { for $sc in xmldb:get-child-collections($collection) let $p := concat($collection, "/", $sc) return map { "type": "collection", "name": $sc, "path": $p, "children": array { if ($level = 0 or $level > $current-level) then (local:get-descendant-resources($p, $current-level + 1)) else () , if ($level = 0 or $level > $current-level) then ( for $r in xmldb:get-child-resources($p) return map { "type": "resource", "name": $r, "path": concat($p, "/", $r) } ) else () } } }; declare function local:tree ($base) { map { "type": "collection", "name": replace($base, ".*/(.+)?", "$1"), "path": $base, "children": array { local:get-descendant-resources($base, 1) } } }; serialize(local:tree($collection), map{"method": "json"}) ` const FILL = '│ ' const ITEM = '├─ ' const LAST = '└─ ' const EMPTY = ' ' function renderItem (name, indent, last, root) { if (root) { return name } if (last) { return indent + LAST + name } return indent + ITEM + name } function getNextIndent (indent, last, root) { if (root) { return indent } if (last) { return indent + EMPTY } return indent + FILL } function renderTree (json, level = 0, indent = '', last = false) { const item = renderItem(json.name, indent, last, level === 0) console.log(item) if (!json.children || json.type === 'resource') { return } const nextLevel = level + 1 const nextIndent = getNextIndent(indent, last, level === 0) const length = json.children.length - 1 for (const i in json.children) { const nextItem = json.children[i] const isLast = Boolean(length === i) renderTree(nextItem, nextLevel, nextIndent, isLast) } } async function tree (collection, level) { try { const db = getXmlRpcClient(connectionOptions) const result = await db.queries.readAll(query, { variables: { collection, level } }) const json = JSON.parse(result.pages.toString()) renderTree(json) return 0 } catch (e) { const message = e.faultString ? e.faultString : e.message console.error('Could not run query! Reason:', message) return 1 } } async function run () { const cli = yargs() .command('$0', 'List a collection and its contents as a tree', (yargs) => { yargs.demandCommand(1, 1).usage(` Usage: exist-tree [options] collection`) }) .option('l', { alias: 'level', describe: 'The maximum depth of the tree. Setting this to zero will output the complete tree.', type: Number, default: 0 }) .option('h', { alias: 'help' }) .strict(false) .exitProcess(false) try { const parsed = cli.parse(argv.slice(2)) if (parsed.help) { return 0 } const { level } = parsed if (typeof level !== 'number' || level < 0) { console.error('Invalid value for option "level"; must be an integer greater than zero.') return 1 } const collection = parsed._ return tree(collection, level) } catch (error) { if (error.name !== 'YError') { console.error(error) } return 1 } } run() .then((exitCode) => { process.exitCode = exitCode }) .catch((error) => { console.error(error) process.exitCode = 1 })