diff --git a/apps/roam/src/utils/__tests__/conceptConversion.test.ts b/apps/roam/src/utils/__tests__/conceptConversion.test.ts new file mode 100644 index 000000000..cde435348 --- /dev/null +++ b/apps/roam/src/utils/__tests__/conceptConversion.test.ts @@ -0,0 +1,48 @@ +import { describe, expect, it, vi } from "vitest"; +import type { SupabaseContext } from "~/utils/supabaseContext"; +import type { DiscourseNode } from "~/utils/getDiscourseNodes"; +import { discourseNodeSchemaToLocalConcept } from "~/utils/conceptConversion"; + +const context = { spaceId: 42 } as SupabaseContext; + +const claimSchema: DiscourseNode = { + type: "schema-1", + text: "Claim", + shortcut: "C", + specification: [], + backedBy: "user", + canvasSettings: {}, + format: "[[CLM]] - {content}", +}; + +const stubRoamQuery = () => { + (globalThis as { window: unknown }).window = { + roamAlphaAPI: { + q: vi.fn().mockReturnValue([["author-1", "page-1", 1000, 2000]]), + }, + }; +}; + +describe("discourseNodeSchemaToLocalConcept", () => { + it("writes label and format into literal_content", () => { + stubRoamQuery(); + const concept = discourseNodeSchemaToLocalConcept(context, claimSchema); + expect(concept.literal_content).toEqual({ + label: "Claim", + format: "[[CLM]] - {content}", + }); + }); + + it("keeps label and format when the type has a template", () => { + stubRoamQuery(); + const concept = discourseNodeSchemaToLocalConcept(context, { + ...claimSchema, + template: [{ text: "Evidence" }], + }); + expect(concept.literal_content).toEqual({ + label: "Claim", + format: "[[CLM]] - {content}", + template: "* Evidence\n", + }); + }); +}); diff --git a/apps/roam/src/utils/__tests__/publishNodesToGroups.test.ts b/apps/roam/src/utils/__tests__/publishNodesToGroups.test.ts index 46dae6534..9930fcaa9 100644 --- a/apps/roam/src/utils/__tests__/publishNodesToGroups.test.ts +++ b/apps/roam/src/utils/__tests__/publishNodesToGroups.test.ts @@ -38,6 +38,7 @@ vi.mock("~/utils/roamToCrossAppConverters", () => ({ label: s.text, authorId: "author-1", createdAt: new Date("2026-01-01T00:00:00.000Z"), + format: s.format, }), reifiedRelationToCrossApp: vi.fn(), relationTripleSchemaToCrossApp: vi.fn(), @@ -172,6 +173,7 @@ describe("publishNodesToGroups", () => { source_local_id: SCHEMA_UID, is_schema: true, name: "Claim", + literal_content: { format: "[[CLM]] - {content}" }, }); expect(data[1]).toMatchObject({ source_local_id: "node-1", diff --git a/apps/roam/src/utils/__tests__/roamToCrossAppConverters.test.ts b/apps/roam/src/utils/__tests__/roamToCrossAppConverters.test.ts index 5596eb602..f81b56f3a 100644 --- a/apps/roam/src/utils/__tests__/roamToCrossAppConverters.test.ts +++ b/apps/roam/src/utils/__tests__/roamToCrossAppConverters.test.ts @@ -9,7 +9,11 @@ vi.mock("roamjs-components/queries/getPageViewType", () => ({ })); vi.mock("~/utils/pageToMarkdown", () => ({ toMarkdown: () => "" })); -import { nodeUidsWithTypeToCrossApp } from "~/utils/roamToCrossAppConverters"; +import { + nodeSchemaToCrossApp, + nodeUidsWithTypeToCrossApp, +} from "~/utils/roamToCrossAppConverters"; +import type { DiscourseNode } from "~/utils/getDiscourseNodes"; const USER_ROW = { ":db/id": 5, ":user/uid": "user-1" }; @@ -60,3 +64,35 @@ describe("nodeUidsWithTypeToCrossApp timestamps", () => { expect(node.modifiedAt).toEqual(new Date(1000)); }); }); + +describe("nodeSchemaToCrossApp", () => { + const claimSchema: DiscourseNode = { + type: "schema-1", + text: "Claim", + shortcut: "C", + specification: [], + backedBy: "user", + canvasSettings: {}, + format: "[[CLM]] - {content}", + }; + + it("carries the node type format", () => { + (globalThis as { window: unknown }).window = { + roamAlphaAPI: { + pull: vi.fn().mockReturnValue({ + ":create/time": 1000, + ":edit/time": 2000, + ":create/user": { ":user/uid": "user-1" }, + }), + }, + }; + const schema = nodeSchemaToCrossApp(claimSchema); + expect(schema).toEqual({ + localId: "schema-1", + label: "Claim", + authorId: "user-1", + createdAt: new Date(1000), + format: "[[CLM]] - {content}", + }); + }); +}); diff --git a/apps/roam/src/utils/conceptConversion.ts b/apps/roam/src/utils/conceptConversion.ts index 83abd1a14..8db580d6f 100644 --- a/apps/roam/src/utils/conceptConversion.ts +++ b/apps/roam/src/utils/conceptConversion.ts @@ -79,22 +79,21 @@ export const discourseNodeSchemaToLocalConcept = ( ): LocalConceptDataInput => { const titleParts = node.text.split("/"); const label = titleParts[titleParts.length - 1] ?? node.text; + const literalContent: { [key: string]: Json } = { + label, + format: node.format, + }; + if (node.template !== undefined) + literalContent.template = templateToText(node.template); const result: LocalConceptDataInput = { space_id: context.spaceId, name: node.text, source_local_id: node.type, is_schema: true, - literal_content: { - label, - }, + literal_content: literalContent, /* eslint-enable @typescript-eslint/naming-convention */ ...getNodeExtraData(node.type), }; - if (node.template !== undefined) - result.literal_content = { - label, - template: templateToText(node.template), - }; return result; }; diff --git a/apps/roam/src/utils/roamToCrossAppConverters.ts b/apps/roam/src/utils/roamToCrossAppConverters.ts index a3c33f399..a3cf29c73 100644 --- a/apps/roam/src/utils/roamToCrossAppConverters.ts +++ b/apps/roam/src/utils/roamToCrossAppConverters.ts @@ -200,5 +200,6 @@ export const nodeSchemaToCrossApp = ( label: s.text, authorId: userUid, createdAt: new Date(relData[":create/time"] || Date.now()), + format: s.format, }; }; diff --git a/packages/database/doc/concept_design.md b/packages/database/doc/concept_design.md index f006842d4..cf496a782 100644 --- a/packages/database/doc/concept_design.md +++ b/packages/database/doc/concept_design.md @@ -114,6 +114,7 @@ Residual (not otherwise accounted for) keys in Obsidian frontmatter are mapped t | `label` | `name` | {} | | `template` | `literal_content->template_content` | | | `templateTitle` | `literal_content->template` | | +| `format` | `literal_content->format` | | | - | `is_schema` | true | | - | `schema_id` | null | | - | `arity` | 0 | diff --git a/packages/database/src/crossAppContracts.ts b/packages/database/src/crossAppContracts.ts index c1bf4268a..a6dfb6a59 100644 --- a/packages/database/src/crossAppContracts.ts +++ b/packages/database/src/crossAppContracts.ts @@ -24,6 +24,7 @@ export type CrossAppNodeSchema = CrossAppSchemaBase & { label: string; template?: string; templateTitle?: string; + format?: string; }; // A relation type schema diff --git a/packages/database/src/lib/__tests__/crossAppConverters.test.ts b/packages/database/src/lib/__tests__/crossAppConverters.test.ts new file mode 100644 index 000000000..1a4d21f11 --- /dev/null +++ b/packages/database/src/lib/__tests__/crossAppConverters.test.ts @@ -0,0 +1,41 @@ +import { describe, expect, it } from "vitest"; +import { crossAppNodeSchemaToDbConcept } from "../crossAppConverters"; +import type { CrossAppNodeSchema } from "../../crossAppContracts"; + +const baseSchema: CrossAppNodeSchema = { + localId: "schema-1", + label: "Claim", + authorId: "author-1", + createdAt: new Date("2026-01-01T00:00:00.000Z"), +}; + +describe("crossAppNodeSchemaToDbConcept", () => { + it("maps format to literal_content.format", () => { + const concept = crossAppNodeSchemaToDbConcept({ + ...baseSchema, + format: "[[CLM]] - {content}", + }); + expect(concept.literal_content).toEqual({ + format: "[[CLM]] - {content}", + }); + }); + + it("keeps the template keys alongside format", () => { + const concept = crossAppNodeSchemaToDbConcept({ + ...baseSchema, + format: "[[CLM]] - {content}", + template: "* Evidence\n", + templateTitle: "Claim template", + }); + expect(concept.literal_content).toEqual({ + format: "[[CLM]] - {content}", + template: "Claim template", + template_content: "* Evidence\n", + }); + }); + + it("omits literal_content when no keys are set", () => { + const concept = crossAppNodeSchemaToDbConcept(baseSchema); + expect(concept.literal_content).toBeUndefined(); + }); +}); diff --git a/packages/database/src/lib/crossAppConverters.ts b/packages/database/src/lib/crossAppConverters.ts index 4427c8f3b..1f92b326d 100644 --- a/packages/database/src/lib/crossAppConverters.ts +++ b/packages/database/src/lib/crossAppConverters.ts @@ -90,6 +90,7 @@ export const crossAppNodeSchemaToDbConcept = ( const literalInfo = filterUndefined({ template: node.templateTitle, template_content: node.template, + format: node.format, }); return filterUndefined({ source_local_id: node.localId,