Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions apps/roam/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -138,7 +138,7 @@ export default runExtension(async (onloadArgs) => {

const { extensionAPI } = onloadArgs;

window.roamjs.extension.queryBuilder = {
const queryBuilderApi = {
runQuery: (parentUid: string) =>
runQuery({ parentUid, extensionAPI }).then(
({ allProcessedResults }) => allProcessedResults,
Expand All @@ -149,9 +149,10 @@ export default runExtension(async (onloadArgs) => {
},
listActiveQueries: () => listActiveQueries(),
isDiscourseNode: isDiscourseNode,
// @ts-expect-error - we are still using roamjs-components global definition
getDiscourseNodes: getDiscourseNodes,
};
window.roamjs?.extension &&
(window.roamjs.extension.queryBuilder = queryBuilderApi);

installDiscourseFloatingMenu(onloadArgs, settings);

Expand Down Expand Up @@ -217,7 +218,7 @@ export default runExtension(async (onloadArgs) => {
cleanups.forEach((fn) => fn());
setSyncActivity(false);
unregisterSlashCommands();
window.roamjs.extension?.smartblocks?.unregisterCommand("QUERYBUILDER");
window.roamjs?.extension?.smartblocks?.unregisterCommand("QUERYBUILDER");
// @ts-expect-error - tldraw throws a warning on multiple loads
delete window[Symbol.for("__signia__")];
document.removeEventListener(
Expand Down
233 changes: 233 additions & 0 deletions apps/roam/src/utils/convertRoamNodeToFullContent.example.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,233 @@
import type { TreeNode } from "roamjs-components/types";
import type { CrossAppNode } from "@repo/database/crossAppNodeContract";
import { buildFullMarkdown } from "./convertRoamNodeToFullContent";

/**
* Typed example for ENG-1848 ("tests or fixtures cover representative Roam
* block content becoming `full` markdown"). This is not a concrete test; it
* documents the `tree.children` shape returned by `getFullTreeByParentUid` for
* a real Roam claim page and type-checks the generated markdown against the
* contract.
*
* Derived from:
* https://roamresearch.com/#/app/plugin-testing-akamatsulab2/page/dnHNmYwe5
*
* Observed markdown output:
*
* # [[CLM]] - Actin assembly peaks 8 seconds before endocytic scission
*
* - ## Source of Claim [ℹ](link to [[@source]], name, URL, etc)
* - [[[[EVD]] - Membrane invagination occurred ~6 seconds after scission protein (spHob1-GFP) was detected in budding yeast cells - [[@sun2019direct]]]]
* - [[[[EVD]] - At fission yeast endocytic sites, scission protein (spHob1) first detected ~2 seconds before initiation of membrane invagination - [[@sun2019direct]]]]
* - [[[[EVD]] - Actin appearance occurred 9 (+/-2) seconds prior to patch formation and disappears 10 (+/-2) seconds following patch development. [[@sirotkin2010quantitative]]]]
* - [[[[EVD]] - At zero seconds, 7000 actin proteins associated with endocytic invagination - [[@sirotkin2010quantitative]]]]
* - actin assembly peaks (x) seconds before/after endocytic scission
* - [[[[CLM]] - Actin assembly peaks 8 seconds before endocytic scission]]
* - ## Notes
* - ![](https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2Fakamatsulab%2F9u2TJGJarR.png?alt=media&token=3c5caff2-7fd5-4df0-94b8-a0d897eb219f)
*/

type SerializedTreeNode = Omit<TreeNode, "children" | "editTime"> & {
children: SerializedTreeNode[];
editTime: string;
};

const deserializeTreeNode = (node: SerializedTreeNode): TreeNode => ({
...node,
editTime: new Date(node.editTime),
children: node.children.map(deserializeTreeNode),
});

const title =
"[[CLM]] - Actin assembly peaks 8 seconds before endocytic scission";

const serializedBlocks: SerializedTreeNode[] = [
{
text: "Source of Claim [ℹ](((JtVWq1Cwl)))",
open: true,
order: 0,
uid: "yxM5yI07g",
heading: 2,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:28:17.505Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [
{
text: "[[[[EVD]] - Membrane invagination occurred ~6 seconds after scission protein (spHob1-GFP) was detected in budding yeast cells - [[@sun2019direct]]]]",
open: true,
order: 0,
uid: "tZnNCIgsk",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-03-25T05:40:43.601Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [],
parents: [0],
},
{
text: "[[[[EVD]] - At fission yeast endocytic sites, scission protein (spHob1) first detected ~2 seconds before initiation of membrane invagination - [[@sun2019direct]]]]",
open: true,
order: 1,
uid: "i-rS06THC",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:38:44.920Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [],
parents: [0],
},
{
text: "[[[[EVD]] - Actin appearance occurred 9 (+/-2) seconds prior to patch formation and disappears 10 (+/-2) seconds following patch development. [[@sirotkin2010quantitative]]]]",
open: true,
order: 2,
uid: "fiWpNvVM8",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:38:54.473Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [],
parents: [0],
},
{
text: "[[[[EVD]] - At zero seconds, 7000 actin proteins associated with endocytic invagination - [[@sirotkin2010quantitative]]]]",
open: true,
order: 3,
uid: "nz8-Sd5a5",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:39:03.540Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [],
parents: [0],
},
{
text: "",
open: true,
order: 4,
uid: "fEcOIBDD7",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:39:31.790Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [],
parents: [0],
},
{
text: "actin assembly peaks (x) seconds before/after endocytic scission",
open: true,
order: 5,
uid: "xgfRsZ-Xb",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:38:25.591Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [
{
text: "[[[[CLM]] - Actin assembly peaks 8 seconds before endocytic scission]]",
open: true,
order: 0,
uid: "URYHjVOwB",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:48:24.324Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [],
parents: [0],
},
],
parents: [0],
},
],
parents: [],
},
{
text: "Notes",
open: true,
order: 1,
uid: "LVRwULrGC",
heading: 2,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-02-29T21:28:17.509Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [
{
text: "![](https://firebasestorage.googleapis.com/v0/b/firescript-577a2.appspot.com/o/imgs%2Fapp%2Fakamatsulab%2F9u2TJGJarR.png?alt=media&token=3c5caff2-7fd5-4df0-94b8-a0d897eb219f)",
open: true,
order: 0,
uid: "aMAPTRui2",
heading: 0,
viewType: "bullet",
blockViewType: "outline",
editTime: "2024-03-07T22:57:13.411Z",
props: {
imageResize: {},
iframe: {},
},
textAlign: "left",
children: [],
parents: [0],
},
],
parents: [],
},
];

const blocks = serializedBlocks.map(deserializeTreeNode);

export const roamClaimFullMarkdownExample: {
title: string;
blocks: TreeNode[];
full: CrossAppNode["content"]["full"];
} = {
title,
blocks,
full: {
format: "text/markdown",
value: buildFullMarkdown({ title, blocks }),
},
};
68 changes: 68 additions & 0 deletions apps/roam/src/utils/convertRoamNodeToFullContent.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
import { toMarkdown } from "./pageToMarkdown";
import { type RoamDiscourseNodeData } from "./getAllDiscourseNodesSince";
import { type DiscourseNode } from "./getDiscourseNodes";
import getFullTreeByParentUid from "roamjs-components/queries/getFullTreeByParentUid";
import getPageViewType from "roamjs-components/queries/getPageViewType";
import type { TreeNode, ViewType } from "roamjs-components/types";
import type { LocalContentDataInput } from "@repo/database/inputTypes";

const FULL_MARKDOWN_OPTS = {
refs: true,
embeds: true,
simplifiedFilename: false,
removeSpecialCharacters: false,
maxFilenameLength: 64,
linkType: "alias",
allNodes: [] as DiscourseNode[],
};

export const buildFullMarkdown = ({
title,
blocks,
viewType = "bullet",
}: {
title: string;
blocks: TreeNode[];
viewType?: ViewType;
}): string => {
const body = blocks
.filter((block) => !!block.text || !!block.children?.length)
.map((block) =>
toMarkdown({ c: block, v: viewType, i: 0, opts: FULL_MARKDOWN_OPTS }),
)
.join("\n")
.trim();
return body ? `# ${title}\n\n${body}\n` : `# ${title}\n`;
};

export const convertRoamNodeToFullContent = ({
nodes,
}: {
nodes: RoamDiscourseNodeData[];
}): LocalContentDataInput[] =>
nodes.flatMap((node) => {
try {
const title = node.node_title ?? node.text;
const blocks = getFullTreeByParentUid(node.source_local_id).children;

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Include the source block in block-backed full markdown

For node types backed by an Embedding Block Ref, getDiscourseNodeTypeWithSettingsBlockNodes sets source_local_id to the child block UID and stores that block's text in node.text; reading only .children here serializes descendants but drops the source block itself. A block-backed discourse node with no children now uploads # <page title> as its full content, and one with children still omits the actual claim/evidence text, so imported materializations are incomplete.

Useful? React with 👍 / 👎.

const viewType = getPageViewType(title) || "bullet";
return [
{
author_local_id: node.author_local_id,
source_local_id: node.source_local_id,
created: new Date(node.created || Date.now()).toISOString(),
last_modified: new Date(
node.last_modified || Date.now(),
).toISOString(),
text: buildFullMarkdown({ title, blocks, viewType }),
variant: "full",
scale: "document",
},
];
} catch (error) {
console.error(
`convertRoamNodeToFullContent: failed to build full markdown for ${node.source_local_id}:`,
error,
);
return [];
Comment on lines +61 to +66

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🚩 Full content silently skipped on error doesn't propagate to caller

When getFullTreeByParentUid or toMarkdown throws for a specific node (line 74-79), the try/catch returns [], meaning that node silently won't have a full content variant uploaded. The contract (crossAppNodeContract.ts:28-31) declares both direct and full as required variants (SHARED_NODE_CONTENT_VARIANTS). If downstream importers strictly validate for the presence of both variants, nodes that failed full generation would appear incomplete. This graceful degradation is intentional per the error log, but worth noting for consumers.

Open in Devin Review

Was this helpful? React with 👍 or 👎 to provide feedback.

}
});
Loading