Skip to content

Commit

Permalink
fix(cli): Bookmark list output is not a valid JSON. Fixes #150 (#181)
Browse files Browse the repository at this point in the history
* 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>
  • Loading branch information
kamtschatka and kamtschatka committed Jun 9, 2024
1 parent 6928800 commit cde9726
Show file tree
Hide file tree
Showing 7 changed files with 241 additions and 78 deletions.
104 changes: 70 additions & 34 deletions apps/cli/src/commands/bookmarks.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand Down Expand Up @@ -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")
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}"`));
});
91 changes: 65 additions & 26 deletions apps/cli/src/commands/lists.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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
Expand All @@ -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}"`,
),
);
});
50 changes: 34 additions & 16 deletions apps/cli/src/commands/tags.ts
Original file line number Diff line number Diff line change
@@ -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";
Expand All @@ -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
Expand All @@ -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}"`));
});
11 changes: 9 additions & 2 deletions apps/cli/src/commands/whoami.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,17 @@
import { printError, printObject } from "@/lib/output";
import { getAPIClient } from "@/lib/trpc";
import { Command } from "@commander-js/extra-typings";

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`,
),
);
});
1 change: 1 addition & 0 deletions apps/cli/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions apps/cli/src/lib/globals.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
export interface GlobalOptions {
apiKey: string;
serverAddr: string;
json?: true;
}

export let globalOpts: GlobalOptions | undefined = undefined;
Expand Down
Loading

0 comments on commit cde9726

Please sign in to comment.