main.cpp 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322
  1. #include <smartbotic/database/client.hpp>
  2. #include <nlohmann/json.hpp>
  3. #include <iostream>
  4. #include <sstream>
  5. #include <string>
  6. #include <vector>
  7. using json = nlohmann::json;
  8. namespace {
  9. struct Args {
  10. std::string address = "localhost:9004";
  11. std::string command;
  12. std::vector<std::string> params;
  13. };
  14. // ANSI colors
  15. constexpr auto C_RESET = "\033[0m";
  16. constexpr auto C_BOLD = "\033[1m";
  17. constexpr auto C_DIM = "\033[2m";
  18. constexpr auto C_CYAN = "\033[36m";
  19. constexpr auto C_GREEN = "\033[32m";
  20. constexpr auto C_RED = "\033[31m";
  21. constexpr auto C_YELLOW = "\033[33m";
  22. void printJson(const json& j) {
  23. std::cout << j.dump(2) << "\n";
  24. }
  25. void printError(const std::string& msg) {
  26. std::cerr << C_RED << "error: " << C_RESET << msg << "\n";
  27. }
  28. void printUsage() {
  29. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET << " — admin tool for smartbotic-database\n\n"
  30. << C_BOLD << "Usage:" << C_RESET << "\n"
  31. << " smartbotic-db-cli [--address HOST:PORT] <command> [args...]\n"
  32. << " smartbotic-db-cli [--address HOST:PORT] " << C_DIM << "# interactive mode" << C_RESET << "\n\n"
  33. << C_BOLD << "Commands:" << C_RESET << "\n"
  34. << " " << C_CYAN << "collections" << C_RESET << " List all collections\n"
  35. << " " << C_CYAN << "info" << C_RESET << " <collection> Collection info\n"
  36. << " " << C_CYAN << "find" << C_RESET << " <collection> List documents\n"
  37. << " " << C_CYAN << "get" << C_RESET << " <collection> <id> Get a document\n"
  38. << " " << C_CYAN << "upsert" << C_RESET << " <collection> <id> '<json>' Insert or update\n"
  39. << " " << C_CYAN << "remove" << C_RESET << " <collection> <id> Delete a document\n"
  40. << " " << C_CYAN << "count" << C_RESET << " <collection> Count documents\n"
  41. << " " << C_CYAN << "lock" << C_RESET << " Lock database (read-only)\n"
  42. << " " << C_CYAN << "unlock" << C_RESET << " Unlock database (accept writes)\n"
  43. << " " << C_CYAN << "status" << C_RESET << " Show read-only + recovery status\n"
  44. << " " << C_CYAN << "help" << C_RESET << " Show this help\n\n"
  45. << C_BOLD << "Options:" << C_RESET << "\n"
  46. << " --address HOST:PORT Database address (default: localhost:9004)\n";
  47. }
  48. bool execCommand(smartbotic::database::Client& client,
  49. const std::string& cmd, const std::vector<std::string>& params) {
  50. try {
  51. if (cmd == "collections") {
  52. auto collections = client.listCollections();
  53. std::cout << C_BOLD << "Collections:" << C_RESET << "\n";
  54. for (const auto& c : collections) {
  55. auto info = client.getCollectionInfo(c);
  56. int64_t count = 0;
  57. if (info) count = info->documentCount;
  58. std::cout << " " << C_CYAN << c << C_RESET
  59. << C_DIM << " (" << count << " docs)" << C_RESET << "\n";
  60. }
  61. return true;
  62. }
  63. if (cmd == "info") {
  64. if (params.empty()) { printError("usage: info <collection>"); return false; }
  65. auto info = client.getCollectionInfo(params[0]);
  66. if (!info) { printError("collection not found: " + params[0]); return false; }
  67. std::cout << C_BOLD << params[0] << C_RESET << ":\n"
  68. << " documents: " << info->documentCount << "\n"
  69. << " size: " << info->sizeBytes << " bytes\n"
  70. << " encrypted: " << (info->encrypted ? "yes" : "no") << "\n"
  71. << " max_versions: " << info->maxVersions << "\n";
  72. if (info->defaultTtlSeconds > 0)
  73. std::cout << " ttl: " << info->defaultTtlSeconds << "s\n";
  74. return true;
  75. }
  76. if (cmd == "find") {
  77. if (params.empty()) { printError("usage: find <collection> [--limit N] [--exists FIELD]"); return false; }
  78. smartbotic::database::Client::QueryOptions opts;
  79. opts.limit = 100;
  80. for (size_t i = 1; i < params.size(); ++i) {
  81. if (params[i] == "--limit" && i + 1 < params.size()) {
  82. opts.limit = static_cast<uint32_t>(std::stoul(params[++i]));
  83. } else if (params[i] == "--exists" && i + 1 < params.size()) {
  84. opts.filters.emplace_back(params[++i], smartbotic::database::Client::FilterOp::EXISTS, true);
  85. }
  86. }
  87. auto docs = client.find(params[0], opts);
  88. std::cout << C_DIM << "(" << docs.size() << " documents)" << C_RESET << "\n";
  89. for (const auto& doc : docs) {
  90. auto id = doc.value("_id", "");
  91. // Print compact summary line
  92. std::cout << C_GREEN << id << C_RESET;
  93. // Show a few key fields
  94. for (const auto& [k, v] : doc.items()) {
  95. if (k == "_id" || k == "_created_at" || k == "_updated_at") continue;
  96. auto val = v.dump();
  97. if (val.size() > 60) val = val.substr(0, 57) + "...";
  98. std::cout << " " << C_DIM << k << "=" << C_RESET << val;
  99. // Limit to 3 fields per line
  100. static int field_count = 0;
  101. if (++field_count >= 3) { field_count = 0; break; }
  102. }
  103. std::cout << "\n";
  104. }
  105. return true;
  106. }
  107. if (cmd == "get") {
  108. if (params.size() < 2) { printError("usage: get <collection> <id>"); return false; }
  109. auto doc = client.get(params[0], params[1]);
  110. if (!doc) { printError("not found: " + params[0] + "/" + params[1]); return false; }
  111. printJson(*doc);
  112. return true;
  113. }
  114. if (cmd == "upsert") {
  115. if (params.size() < 3) { printError("usage: upsert <collection> <id> '<json>'"); return false; }
  116. auto data = json::parse(params[2], nullptr, false);
  117. if (!data.is_object()) { printError("invalid JSON: " + params[2]); return false; }
  118. client.upsert(params[0], data, params[1]);
  119. std::cout << C_GREEN << "ok" << C_RESET << " " << params[0] << "/" << params[1] << "\n";
  120. return true;
  121. }
  122. if (cmd == "remove" || cmd == "delete") {
  123. if (params.size() < 2) { printError("usage: remove <collection> <id>"); return false; }
  124. client.remove(params[0], params[1]);
  125. std::cout << C_GREEN << "ok" << C_RESET << " removed " << params[0] << "/" << params[1] << "\n";
  126. return true;
  127. }
  128. if (cmd == "count") {
  129. if (params.empty()) { printError("usage: count <collection>"); return false; }
  130. auto info = client.getCollectionInfo(params[0]);
  131. if (!info) { printError("collection not found: " + params[0]); return false; }
  132. std::cout << info->documentCount << "\n";
  133. return true;
  134. }
  135. if (cmd == "lock") {
  136. if (client.setReadOnly(true)) {
  137. std::cout << C_GREEN << "ok" << C_RESET << " Database locked (read-only)\n";
  138. return true;
  139. }
  140. printError("failed to lock database");
  141. return false;
  142. }
  143. if (cmd == "unlock") {
  144. if (client.setReadOnly(false)) {
  145. std::cout << C_GREEN << "ok" << C_RESET << " Database unlocked (writes accepted)\n";
  146. return true;
  147. }
  148. printError("failed to unlock database");
  149. return false;
  150. }
  151. if (cmd == "status") {
  152. auto s = client.getReadOnlyStatus();
  153. std::cout << "Read-only: " << (s.readOnly ? (std::string(C_RED) + "YES" + C_RESET) : "no") << "\n";
  154. if (s.readOnly) {
  155. std::cout << "Reason: " << s.reason << "\n";
  156. }
  157. std::cout << "Recovery outcome: " << s.recoveryOutcome << "\n";
  158. if (!s.expectedSnapshot.empty()) {
  159. std::cout << "Expected snapshot: " << s.expectedSnapshot << "\n";
  160. }
  161. if (!s.snapshotUsed.empty()) {
  162. std::cout << "Snapshot used: " << s.snapshotUsed << "\n";
  163. }
  164. if (!s.failureReason.empty()) {
  165. std::cout << "Failure reason: " << s.failureReason << "\n";
  166. }
  167. std::cout << "WAL replayed: " << s.walEntriesReplayed << " entries\n";
  168. std::cout << "Snapshots tried: " << s.snapshotsAttempted << "\n";
  169. return true;
  170. }
  171. if (cmd == "help" || cmd == "?") {
  172. printUsage();
  173. return true;
  174. }
  175. printError("unknown command: " + cmd + " (try 'help')");
  176. return false;
  177. } catch (const std::exception& e) {
  178. printError(e.what());
  179. return false;
  180. }
  181. }
  182. // Tokenize a line, respecting single/double quotes and JSON braces
  183. std::vector<std::string> tokenize(const std::string& line) {
  184. std::vector<std::string> tokens;
  185. std::string current;
  186. int brace_depth = 0;
  187. char in_quote = 0;
  188. for (size_t i = 0; i < line.size(); ++i) {
  189. char c = line[i];
  190. if (in_quote) {
  191. current += c;
  192. if (c == in_quote && (i == 0 || line[i-1] != '\\')) {
  193. in_quote = 0;
  194. // Strip surrounding quotes for simple string tokens
  195. if (brace_depth == 0 && current.size() >= 2
  196. && (current.front() == '\'' || current.front() == '"')
  197. && current.front() == current.back()) {
  198. current = current.substr(1, current.size() - 2);
  199. }
  200. }
  201. continue;
  202. }
  203. if (c == '\'' || c == '"') {
  204. in_quote = c;
  205. current += c;
  206. continue;
  207. }
  208. if (c == '{') { brace_depth++; current += c; continue; }
  209. if (c == '}') {
  210. brace_depth--;
  211. current += c;
  212. if (brace_depth <= 0) {
  213. brace_depth = 0;
  214. tokens.push_back(current);
  215. current.clear();
  216. }
  217. continue;
  218. }
  219. if (brace_depth > 0) { current += c; continue; }
  220. if (c == ' ' || c == '\t') {
  221. if (!current.empty()) {
  222. tokens.push_back(current);
  223. current.clear();
  224. }
  225. continue;
  226. }
  227. current += c;
  228. }
  229. if (!current.empty()) tokens.push_back(current);
  230. return tokens;
  231. }
  232. } // anonymous namespace
  233. int main(int argc, char* argv[]) {
  234. Args args;
  235. // Parse flags
  236. std::vector<std::string> positional;
  237. for (int i = 1; i < argc; ++i) {
  238. std::string arg = argv[i];
  239. if (arg == "--address" && i + 1 < argc) {
  240. args.address = argv[++i];
  241. } else if (arg == "--help" || arg == "-h") {
  242. printUsage();
  243. return 0;
  244. } else {
  245. positional.push_back(arg);
  246. }
  247. }
  248. if (!positional.empty()) {
  249. args.command = positional[0];
  250. args.params.assign(positional.begin() + 1, positional.end());
  251. }
  252. // Connect
  253. smartbotic::database::Client client({.address = args.address});
  254. client.connect();
  255. // Scriptable mode: single command
  256. if (!args.command.empty()) {
  257. return execCommand(client, args.command, args.params) ? 0 : 1;
  258. }
  259. // Interactive mode
  260. std::cout << C_BOLD << "smartbotic-db-cli" << C_RESET
  261. << " connected to " << C_CYAN << args.address << C_RESET << "\n"
  262. << C_DIM << "Type 'help' for commands, 'exit' to quit." << C_RESET << "\n";
  263. std::string line;
  264. while (true) {
  265. std::cout << C_YELLOW << "> " << C_RESET;
  266. if (!std::getline(std::cin, line)) break;
  267. // Trim
  268. auto start = line.find_first_not_of(" \t");
  269. if (start == std::string::npos) continue;
  270. line = line.substr(start);
  271. if (line == "exit" || line == "quit" || line == "q") break;
  272. if (line.empty()) continue;
  273. auto tokens = tokenize(line);
  274. if (tokens.empty()) continue;
  275. auto cmd = tokens[0];
  276. std::vector<std::string> params(tokens.begin() + 1, tokens.end());
  277. execCommand(client, cmd, params);
  278. }
  279. return 0;
  280. }