rcgit

/ karakeep

Commit cde97267

SHA cde97267a90802c6a367aa61ff157983506deead
Author kamtschatka <sschatka at gmail dot com>
Author Date 2024-06-09 23:30 +0200
Committer GitHub <noreply at github dot com>
Commit Date 2024-06-09 22:30 +0100
Parent(s) 6928800a604f (diff)
Tree 7fc6dbca60ab

patch snapshot

fix(cli): Bookmark list output is not a valid JSON. Fixes #150 (#181)
* bookmark list output is not a valid JSON #150
Reworked the cli to switch over to json output

* changed the logging to log created bookmarks as an array
switch all log output that is just a status to stderr

---------

Co-authored-by: kamtschatka <simon.schatka@gmx.at>
File + - Graph
M apps/cli/src/commands/bookmarks.ts +70 -34
M apps/cli/src/commands/lists.ts +65 -26
M apps/cli/src/commands/tags.ts +34 -16
M apps/cli/src/commands/whoami.ts +9 -2
M apps/cli/src/index.ts +1 -0
M apps/cli/src/lib/globals.ts +1 -0
A apps/cli/src/lib/output.ts +61 -0
7 file(s) changed, 241 insertions(+), 78 deletions(-)

apps/cli/src/commands/bookmarks.ts

diff --git a/apps/cli/src/commands/bookmarks.ts b/apps/cli/src/commands/bookmarks.ts
index 0f557120..40442ec1 100644
--- a/apps/cli/src/commands/bookmarks.ts
+++ b/apps/cli/src/commands/bookmarks.ts
@@ -1,7 +1,12 @@
 import * as fs from "node:fs";
+import {
+  printError,
+  printObject,
+  printStatusMessage,
+  printSuccess,
+} from "@/lib/output";
 import { getAPIClient } from "@/lib/trpc";
 import { Command } from "@commander-js/extra-typings";
-import chalk from "chalk";
 
 import type { ZBookmark } from "@hoarder/shared/types/bookmarks";
 import { MAX_NUM_BOOKMARKS_PER_PAGE } from "@hoarder/shared/types/bookmarks";
@@ -30,6 +35,10 @@ function normalizeBookmark(bookmark: ZBookmark) {
   return ret;
 }
 
+function printBookmark(bookmark: ZBookmark) {
+  printObject(normalizeBookmark(bookmark));
+}
+
 bookmarkCmd
   .command("add")
   .description("creates a new bookmark")
@@ -49,31 +58,49 @@ bookmarkCmd
   .action(async (opts) => {
     const api = getAPIClient();
 
+    const results: object[] = [];
+
     const promises = [
       ...opts.link.map((url) =>
-        api.bookmarks.createBookmark.mutate({ type: "link", url }),
+        api.bookmarks.createBookmark
+          .mutate({ type: "link", url })
+          .then((bookmark: ZBookmark) => {
+            results.push(normalizeBookmark(bookmark));
+          })
+          .catch(printError(`Failed to add a link bookmark for url "${url}"`)),
       ),
       ...opts.note.map((text) =>
-        api.bookmarks.createBookmark.mutate({ type: "text", text }),
+        api.bookmarks.createBookmark
+          .mutate({ type: "text", text })
+          .then((bookmark: ZBookmark) => {
+            results.push(normalizeBookmark(bookmark));
+          })
+          .catch(
+            printError(
+              `Failed to add a text bookmark with text "${text.substring(0, 50)}"`,
+            ),
+          ),
       ),
     ];
 
     if (opts.stdin) {
       const text = fs.readFileSync(0, "utf-8");
       promises.push(
-        api.bookmarks.createBookmark.mutate({ type: "text", text }),
+        api.bookmarks.createBookmark
+          .mutate({ type: "text", text })
+          .then((bookmark: ZBookmark) => {
+            results.push(normalizeBookmark(bookmark));
+          })
+          .catch(
+            printError(
+              `Failed to add a text bookmark with text "${text.substring(0, 50)}"`,
+            ),
+          ),
       );
     }
 
-    const results = await Promise.allSettled(promises);
-
-    for (const res of results) {
-      if (res.status == "fulfilled") {
-        console.log(normalizeBookmark(res.value));
-      } else {
-        console.log(chalk.red(`Error: ${res.reason}`));
-      }
-    }
+    await Promise.allSettled(promises);
+    printObject(results);
   });
 
 bookmarkCmd
@@ -82,8 +109,10 @@ bookmarkCmd
   .argument("<id>", "The id of the bookmark to get")
   .action(async (id) => {
     const api = getAPIClient();
-    const resp = await api.bookmarks.getBookmark.query({ bookmarkId: id });
-    console.log(normalizeBookmark(resp));
+    await api.bookmarks.getBookmark
+      .query({ bookmarkId: id })
+      .then(printBookmark)
+      .catch(printError(`Failed to get the bookmark with id "${id}"`));
   });
 
 bookmarkCmd
@@ -98,13 +127,15 @@ bookmarkCmd
   .argument("<id>", "the id of the bookmark to get")
   .action(async (id, opts) => {
     const api = getAPIClient();
-    const resp = await api.bookmarks.updateBookmark.mutate({
-      bookmarkId: id,
-      archived: opts.archive,
-      favourited: opts.favourite,
-      title: opts.title,
-    });
-    console.log(resp);
+    await api.bookmarks.updateBookmark
+      .mutate({
+        bookmarkId: id,
+        archived: opts.archive,
+        favourited: opts.favourite,
+        title: opts.title,
+      })
+      .then(printObject)
+      .catch(printError(`Failed to update bookmark with id "${id}"`));
   });
 
 bookmarkCmd
@@ -126,18 +157,21 @@ bookmarkCmd
       useCursorV2: true,
     };
 
-    let resp = await api.bookmarks.getBookmarks.query(request);
-    let results: ZBookmark[] = resp.bookmarks;
+    try {
+      let resp = await api.bookmarks.getBookmarks.query(request);
+      let results: ZBookmark[] = resp.bookmarks;
 
-    while (resp.nextCursor) {
-      resp = await api.bookmarks.getBookmarks.query({
-        ...request,
-        cursor: resp.nextCursor,
-      });
-      results = [...results, ...resp.bookmarks];
+      while (resp.nextCursor) {
+        resp = await api.bookmarks.getBookmarks.query({
+          ...request,
+          cursor: resp.nextCursor,
+        });
+        results = [...results, ...resp.bookmarks];
+      }
+      printObject(results.map(normalizeBookmark), { maxArrayLength: null });
+    } catch (e) {
+      printStatusMessage(false, "Failed to query bookmarks");
     }
-
-    console.dir(results.map(normalizeBookmark), { maxArrayLength: null });
   });
 
 bookmarkCmd
@@ -146,6 +180,8 @@ bookmarkCmd
   .argument("<id>", "the id of the bookmark to delete")
   .action(async (id) => {
     const api = getAPIClient();
-    await api.bookmarks.deleteBookmark.mutate({ bookmarkId: id });
-    console.log(`Bookmark ${id} got deleted`);
+    await api.bookmarks.deleteBookmark
+      .mutate({ bookmarkId: id })
+      .then(printSuccess(`Bookmark with id '${id}' got deleted`))
+      .catch(printError(`Failed to delete bookmark with id "${id}"`));
   });

apps/cli/src/commands/lists.ts

diff --git a/apps/cli/src/commands/lists.ts b/apps/cli/src/commands/lists.ts
index c7b2a5f0..2f85ae7b 100644
--- a/apps/cli/src/commands/lists.ts
+++ b/apps/cli/src/commands/lists.ts
@@ -1,3 +1,10 @@
+import { getGlobalOptions } from "@/lib/globals";
+import {
+  printError,
+  printErrorMessageWithReason,
+  printObject,
+  printSuccess,
+} from "@/lib/output";
 import { getAPIClient } from "@/lib/trpc";
 import { Command } from "@commander-js/extra-typings";
 import { getBorderCharacters, table } from "table";
@@ -14,19 +21,30 @@ listsCmd
   .action(async () => {
     const api = getAPIClient();
 
-    const resp = await api.lists.list.query();
-    const { allPaths } = listsToTree(resp.lists);
+    try {
+      const resp = await api.lists.list.query();
 
-    const data: string[][] = [["Id", "Name"]];
+      if (getGlobalOptions().json) {
+        printObject(resp);
+      } else {
+        const { allPaths } = listsToTree(resp.lists);
+        const data: string[][] = [["Id", "Name"]];
 
-    allPaths.forEach((path) => {
-      const name = path.map((p) => `${p.icon} ${p.name}`).join(" / ");
-      const id = path[path.length - 1].id;
-      data.push([id, name]);
-    });
-    console.log(
-      table(data, { border: getBorderCharacters("ramac"), singleLine: true }),
-    );
+        allPaths.forEach((path) => {
+          const name = path.map((p) => `${p.icon} ${p.name}`).join(" / ");
+          const id = path[path.length - 1].id;
+          data.push([id, name]);
+        });
+        console.log(
+          table(data, {
+            border: getBorderCharacters("ramac"),
+            singleLine: true,
+          }),
+        );
+      }
+    } catch (error) {
+      printErrorMessageWithReason("Failed to list all lists", error as object);
+    }
   });
 
 listsCmd
@@ -36,10 +54,12 @@ listsCmd
   .action(async (id) => {
     const api = getAPIClient();
 
-    await api.lists.delete.mutate({
-      listId: id,
-    });
-    console.log("Successfully deleted list with id:", id);
+    await api.lists.delete
+      .mutate({
+        listId: id,
+      })
+      .then(printSuccess(`Successfully deleted list with id "${id}"`))
+      .catch(printError(`Failed to delete list with id "${id}"`));
   });
 
 listsCmd
@@ -50,11 +70,21 @@ listsCmd
   .action(async (opts) => {
     const api = getAPIClient();
 
-    await api.lists.addToList.mutate({
-      listId: opts.list,
-      bookmarkId: opts.bookmark,
-    });
-    console.log("Successfully added bookmark from list");
+    await api.lists.addToList
+      .mutate({
+        listId: opts.list,
+        bookmarkId: opts.bookmark,
+      })
+      .then(
+        printSuccess(
+          `Successfully added bookmark "${opts.bookmark}" to list with id "${opts.list}"`,
+        ),
+      )
+      .catch(
+        printError(
+          `Failed to add bookmark "${opts.bookmark}" to list with id "${opts.list}"`,
+        ),
+      );
   });
 
 listsCmd
@@ -65,10 +95,19 @@ listsCmd
   .action(async (opts) => {
     const api = getAPIClient();
 
-    await api.lists.removeFromList.mutate({
-      listId: opts.list,
-      bookmarkId: opts.bookmark,
-    });
-
-    console.log("Successfully removed bookmark from list");
+    await api.lists.removeFromList
+      .mutate({
+        listId: opts.list,
+        bookmarkId: opts.bookmark,
+      })
+      .then(
+        printSuccess(
+          `Successfully removed bookmark "${opts.bookmark}" from list with id "${opts.list}"`,
+        ),
+      )
+      .catch(
+        printError(
+          `Failed to remove bookmark "${opts.bookmark}" from list with id "${opts.list}"`,
+        ),
+      );
   });

apps/cli/src/commands/tags.ts

diff --git a/apps/cli/src/commands/tags.ts b/apps/cli/src/commands/tags.ts
index 410f1abd..c2c1dd3a 100644
--- a/apps/cli/src/commands/tags.ts
+++ b/apps/cli/src/commands/tags.ts
@@ -1,3 +1,10 @@
+import { getGlobalOptions } from "@/lib/globals";
+import {
+  printError,
+  printErrorMessageWithReason,
+  printObject,
+  printSuccess,
+} from "@/lib/output";
 import { getAPIClient } from "@/lib/trpc";
 import { Command } from "@commander-js/extra-typings";
 import { getBorderCharacters, table } from "table";
@@ -12,17 +19,27 @@ tagsCmd
   .action(async () => {
     const api = getAPIClient();
 
-    const tags = (await api.tags.list.query()).tags;
-    tags.sort((a, b) => b.count - a.count);
-
-    const data: string[][] = [["Id", "Name", "Num bookmarks"]];
-
-    tags.forEach((tag) => {
-      data.push([tag.id, tag.name, tag.count.toString()]);
-    });
-    console.log(
-      table(data, { border: getBorderCharacters("ramac"), singleLine: true }),
-    );
+    try {
+      const tags = (await api.tags.list.query()).tags;
+      tags.sort((a, b) => b.count - a.count);
+      if (getGlobalOptions().json) {
+        printObject(tags);
+      } else {
+        const data: string[][] = [["Id", "Name", "Num bookmarks"]];
+
+        tags.forEach((tag) => {
+          data.push([tag.id, tag.name, tag.count.toString()]);
+        });
+        console.log(
+          table(data, {
+            border: getBorderCharacters("ramac"),
+            singleLine: true,
+          }),
+        );
+      }
+    } catch (error) {
+      printErrorMessageWithReason("Failed to list all tags", error as object);
+    }
   });
 
 tagsCmd
@@ -32,9 +49,10 @@ tagsCmd
   .action(async (id) => {
     const api = getAPIClient();
 
-    await api.tags.delete.mutate({
-      tagId: id,
-    });
-
-    console.log("Successfully delete the tag with id:", id);
+    await api.tags.delete
+      .mutate({
+        tagId: id,
+      })
+      .then(printSuccess(`Successfully deleted the tag with the id "${id}"`))
+      .catch(printError(`Failed to delete the tag with the id "${id}"`));
   });

apps/cli/src/commands/whoami.ts

diff --git a/apps/cli/src/commands/whoami.ts b/apps/cli/src/commands/whoami.ts
index b55bfa67..06a94e8f 100644
--- a/apps/cli/src/commands/whoami.ts
+++ b/apps/cli/src/commands/whoami.ts
@@ -1,3 +1,4 @@
+import { printError, printObject } from "@/lib/output";
 import { getAPIClient } from "@/lib/trpc";
 import { Command } from "@commander-js/extra-typings";
 
@@ -5,6 +6,12 @@ export const whoamiCmd = new Command()
   .name("whoami")
   .description("returns info about the owner of this API key")
   .action(async () => {
-    const resp = await getAPIClient().users.whoami.query();
-    console.log(resp);
+    await getAPIClient()
+      .users.whoami.query()
+      .then(printObject)
+      .catch(
+        printError(
+          `Unable to fetch information about the owner of this API key`,
+        ),
+      );
   });

apps/cli/src/index.ts

diff --git a/apps/cli/src/index.ts b/apps/cli/src/index.ts
index a4f6f7b4..a2a21a75 100644
--- a/apps/cli/src/index.ts
+++ b/apps/cli/src/index.ts
@@ -22,6 +22,7 @@ const program = new Command()
       .makeOptionMandatory(true)
       .env("HOARDER_SERVER_ADDR"),
   )
+  .addOption(new Option("--json", "to output the result as JSON"))
   .version(
     import.meta.env && "CLI_VERSION" in import.meta.env
       ? import.meta.env.CLI_VERSION

apps/cli/src/lib/globals.ts

diff --git a/apps/cli/src/lib/globals.ts b/apps/cli/src/lib/globals.ts
index 771136da..8a301cfe 100644
--- a/apps/cli/src/lib/globals.ts
+++ b/apps/cli/src/lib/globals.ts
@@ -1,6 +1,7 @@
 export interface GlobalOptions {
   apiKey: string;
   serverAddr: string;
+  json?: true;
 }
 
 export let globalOpts: GlobalOptions | undefined = undefined;

apps/cli/src/lib/output.ts

diff --git a/apps/cli/src/lib/output.ts b/apps/cli/src/lib/output.ts
new file mode 100644
index 00000000..34d86461
--- /dev/null
+++ b/apps/cli/src/lib/output.ts
@@ -0,0 +1,61 @@
+import { InspectOptions } from "util";
+import chalk from "chalk";
+
+import { getGlobalOptions } from "./globals";
+
+/**
+ * Prints an object either in a nicely formatted way or as JSON (depending on the command flag --json)
+ *
+ * @param output
+ */
+export function printObject(
+  output: object,
+  extraOptions?: InspectOptions,
+): void {
+  if (getGlobalOptions().json) {
+    console.log(JSON.stringify(output, undefined, 4));
+  } else {
+    console.dir(output, extraOptions);
+  }
+}
+
+/**
+ * Used to output a status (success/error) and a message either as string or as JSON (depending on the command flag --json)
+ *
+ * @param success if the message is a successful message or an error
+ * @param output the message to output
+ */
+export function printStatusMessage(success: boolean, message: unknown): void {
+  const status = success ? "Success" : "Error";
+  const colorFunction = success ? chalk.green : chalk.red;
+  console.error(colorFunction(`${status}: ${message}`));
+}
+
+/**
+ * @param message The message that will be printed as a successful message
+ * @returns a function that can be used in a Promise on success
+ */
+export function printSuccess(message: string) {
+  return () => {
+    printStatusMessage(true, message);
+  };
+}
+
+/**
+ * @param message The message that will be printed as an error message
+ * @returns a function that can be used in a Promise on rejection
+ */
+export function printError(message: string) {
+  return (error: object) => {
+    printErrorMessageWithReason(message, error);
+  };
+}
+
+/**
+ * @param message The message that will be printed as an error message
+ * @param error an error object with the reason for the error
+ */
+export function printErrorMessageWithReason(message: string, error: object) {
+  const errorMessage = "message" in error ? error.message : error;
+  printStatusMessage(false, `${message}. Reason: ${errorMessage}`);
+}