From f6765d15c679f03b5cf807740fcb5f68b007c210 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 23 Feb 2026 00:24:28 +0530 Subject: [PATCH 01/10] ENG-1455: Dual-read from old-system settings and from blockprops --- .../components/settings/utils/accessors.ts | 668 +++++++++++++++++- .../settings/utils/zodSchema.example.ts | 4 +- .../components/settings/utils/zodSchema.ts | 21 +- apps/roam/src/index.ts | 6 + 4 files changed, 670 insertions(+), 29 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index cc5a0844f..f0933ffe6 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -3,9 +3,15 @@ import getBlockProps, { type json, } from "~/utils/getBlockProps"; import setBlockProps from "~/utils/setBlockProps"; +import getBasicTreeByParentUid from "roamjs-components/queries/getBasicTreeByParentUid"; import getBlockUidByTextOnPage from "roamjs-components/queries/getBlockUidByTextOnPage"; import getPageUidByPageTitle from "roamjs-components/queries/getPageUidByPageTitle"; +import { getSubTree } from "roamjs-components/util"; +import getSettingValueFromTree from "roamjs-components/util/getSettingValueFromTree"; import internalError from "~/utils/internalError"; +import { getSetting } from "~/utils/extensionSettings"; +import { getFormattedConfigTree } from "~/utils/discourseConfigRef"; +import { roamNodeToCondition } from "~/utils/parseQuery"; import { z } from "zod"; import { @@ -106,6 +112,23 @@ const getSchemaAtPath = ( const formatSettingPath = (keys: string[]): string => keys.length === 0 ? "(root)" : keys.join(" > "); +const readPathValue = (root: unknown, keys: string[]): unknown => + keys.reduce((current, key) => { + if (!isRecord(current) || !(key in current)) return undefined; + return current[key]; + }, root); + +const pathKey = (keys: string[]): string => keys.join("::"); + +const getMissingSettingError = ({ + context, + keys, +}: { + context: string; + keys: string[]; +}): string => + `[DG Accessor] Missing ${context} setting at path: ${formatSettingPath(keys)} (dual-read ON)`; + const validateSettingValue = ({ schema, keys, @@ -142,6 +165,368 @@ const validateSettingValue = ({ return true; }; +const DEFAULT_PERSONAL_SETTINGS = PersonalSettingsSchema.parse({}); +const DEFAULT_GLOBAL_SETTINGS = GlobalSettingsSchema.parse({}); +const DEFAULT_LEGACY_QUERY = { + conditions: [], + selections: [], + custom: "", + returnNode: "node", +}; + +const PERSONAL_OLD_KEY_MAP = new Map([ + [pathKey(["Discourse context overlay"]), "discourse-context-overlay"], + [pathKey(["Suggestive mode overlay"]), "suggestive-mode-overlay"], + [pathKey(["Text selection popup"]), "text-selection-popup"], + [pathKey(["Disable sidebar open"]), "disable-sidebar-open"], + [pathKey(["Page preview"]), "page-preview"], + [pathKey(["Hide feedback button"]), "hide-feedback-button"], + [pathKey(["Auto canvas relations"]), "auto-canvas-relations"], + [pathKey(["Overlay in canvas"]), "discourse-context-overlay-in-canvas"], + [pathKey(["Streamline styling"]), "streamline-styling"], + [pathKey(["Disable product diagnostics"]), "disallow-diagnostics"], + [pathKey(["Discourse tool shortcut"]), "discourse-tool-shortcut"], + [pathKey(["Personal node menu trigger"]), "personal-node-menu-trigger"], + [pathKey(["Node search menu trigger"]), "node-search-trigger"], + [pathKey(["Query", "Hide query metadata"]), "hide-metadata"], + [pathKey(["Query", "Default page size"]), "default-page-size"], + [pathKey(["Query", "Query pages"]), "query-pages"], + [pathKey(["Query", "Default filters"]), "default-filters"], +]); + +const PERSONAL_OLD_DEFAULT_OVERRIDES = new Map([ + [pathKey(["Query", "Hide query metadata"]), true], +]); + +const getLegacyPersonalLeftSidebarSetting = (): Record => { + const settings = getFormattedConfigTree(); + + /* eslint-disable @typescript-eslint/naming-convention */ + return Object.fromEntries( + settings.leftSidebar.personal.sections.map((section) => [ + section.text, + { + Children: (section.children || []).map((child) => ({ + Page: child.text, + Alias: child.alias?.value || "", + })), + Settings: { + "Truncate-result?": section.settings?.truncateResult.value ?? 75, + Folded: section.settings?.folded.value ?? false, + }, + }, + ]), + ); + /* eslint-enable @typescript-eslint/naming-convention */ +}; + +const getLegacyPersonalSetting = (keys: string[]): unknown => { + const mappedOldKey = PERSONAL_OLD_KEY_MAP.get(pathKey(keys)); + if (mappedOldKey) { + const path = pathKey(keys); + return getSetting( + mappedOldKey, + PERSONAL_OLD_DEFAULT_OVERRIDES.has(path) + ? PERSONAL_OLD_DEFAULT_OVERRIDES.get(path) + : readPathValue(DEFAULT_PERSONAL_SETTINGS, keys), + ); + } + + if (keys.length === 1 && keys[0] === "Query") { + const querySettings: Record = {}; + querySettings["Hide query metadata"] = getLegacyPersonalSetting([ + "Query", + "Hide query metadata", + ]); + querySettings["Default page size"] = getLegacyPersonalSetting([ + "Query", + "Default page size", + ]); + querySettings["Query pages"] = getLegacyPersonalSetting([ + "Query", + "Query pages", + ]); + querySettings["Default filters"] = getLegacyPersonalSetting([ + "Query", + "Default filters", + ]); + return querySettings; + } + + if (keys[0] === "Left sidebar") { + const leftSidebarSettings = getLegacyPersonalLeftSidebarSetting(); + if (keys.length === 1) return leftSidebarSettings; + return readPathValue(leftSidebarSettings, keys.slice(1)); + } + + return undefined; +}; + +const getLegacyRelationsSetting = (): Record => { + const settingsUid = getPageUidByPageTitle(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE); + if (!settingsUid) return DEFAULT_GLOBAL_SETTINGS.Relations; + + const configTree = getBasicTreeByParentUid(settingsUid); + const grammarChildren = getSubTree({ + tree: configTree, + key: "grammar", + }).children; + const relationNodes = getSubTree({ + tree: grammarChildren, + key: "relations", + }).children; + if (relationNodes.length === 0) return DEFAULT_GLOBAL_SETTINGS.Relations; + + return Object.fromEntries( + relationNodes.map((relationNode) => { + const relationTree = relationNode.children || []; + const ifBlocks = getSubTree({ tree: relationTree, key: "If" }).children; + const ifConditions = ifBlocks.map((ifBlock) => { + const blockChildren = ifBlock.children || []; + const nodePositionsNode = blockChildren.find((c) => + /node positions/i.test(c.text), + ); + const triples = blockChildren + .filter((c) => !/node positions/i.test(c.text)) + .map( + (c) => + [ + c.text, + c.children?.[0]?.text || "", + c.children?.[0]?.children?.[0]?.text || "", + ] as [string, string, string], + ); + const nodePositions = Object.fromEntries( + (nodePositionsNode?.children || []).map((c) => [ + c.text, + c.children?.[0]?.text || "", + ]), + ); + return { triples, nodePositions }; + }); + + return [ + relationNode.uid || relationNode.text, + { + label: relationNode.text, + source: getSettingValueFromTree({ + tree: relationTree, + key: "Source", + }), + destination: getSettingValueFromTree({ + tree: relationTree, + key: "Destination", + }), + complement: getSettingValueFromTree({ + tree: relationTree, + key: "Complement", + }), + ifConditions, + }, + ]; + }), + ); +}; + +const getLegacyGlobalSetting = (keys: string[]): unknown => { + if (keys.length === 0) return undefined; + + const settings = getFormattedConfigTree(); + const firstKey = keys[0]; + + if (firstKey === "Trigger") { + return settings.trigger.value || DEFAULT_GLOBAL_SETTINGS.Trigger; + } + + if (firstKey === "Canvas page format") { + return ( + settings.canvasPageFormat.value || + DEFAULT_GLOBAL_SETTINGS["Canvas page format"] + ); + } + + if (firstKey === "Left sidebar") { + const leftSidebarSettings: Record = {}; + leftSidebarSettings["Children"] = settings.leftSidebar.global.children.map( + (c) => c.text, + ); + const sidebarSettingValues: Record = {}; + sidebarSettingValues["Collapsable"] = + settings.leftSidebar.global.settings?.collapsable.value ?? + DEFAULT_GLOBAL_SETTINGS["Left sidebar"].Settings.Collapsable; + sidebarSettingValues["Folded"] = + settings.leftSidebar.global.settings?.folded.value ?? + DEFAULT_GLOBAL_SETTINGS["Left sidebar"].Settings.Folded; + leftSidebarSettings["Settings"] = sidebarSettingValues; + if (keys.length === 1) return leftSidebarSettings; + return readPathValue(leftSidebarSettings, keys.slice(1)); + } + + if (firstKey === "Export") { + const exportSettings: Record = {}; + exportSettings["Remove special characters"] = + settings.export.removeSpecialCharacters.value ?? + DEFAULT_GLOBAL_SETTINGS.Export["Remove special characters"]; + exportSettings["Resolve block references"] = + settings.export.optsRefs.value ?? + DEFAULT_GLOBAL_SETTINGS.Export["Resolve block references"]; + exportSettings["Resolve block embeds"] = + settings.export.optsEmbeds.value ?? + DEFAULT_GLOBAL_SETTINGS.Export["Resolve block embeds"]; + exportSettings["Append referenced node"] = + settings.export.appendRefNodeContext.value ?? + DEFAULT_GLOBAL_SETTINGS.Export["Append referenced node"]; + exportSettings["Link type"] = + settings.export.linkType.value || + DEFAULT_GLOBAL_SETTINGS.Export["Link type"]; + exportSettings["Max filename length"] = + settings.export.maxFilenameLength.value ?? + DEFAULT_GLOBAL_SETTINGS.Export["Max filename length"]; + exportSettings["Frontmatter"] = + settings.export.frontmatter.values ?? + DEFAULT_GLOBAL_SETTINGS.Export.Frontmatter; + if (keys.length === 1) return exportSettings; + return readPathValue(exportSettings, keys.slice(1)); + } + + if (firstKey === "Suggestive mode") { + const suggestiveModeSettings: Record = {}; + suggestiveModeSettings["Include current page relations"] = + settings.suggestiveMode.includePageRelations.value ?? + DEFAULT_GLOBAL_SETTINGS["Suggestive mode"][ + "Include current page relations" + ]; + suggestiveModeSettings["Include parent and child blocks"] = + settings.suggestiveMode.includeParentAndChildren.value ?? + DEFAULT_GLOBAL_SETTINGS["Suggestive mode"][ + "Include parent and child blocks" + ]; + suggestiveModeSettings["Page groups"] = + settings.suggestiveMode.pageGroups.groups.map((group) => ({ + name: group.name, + pages: group.pages.map((page) => page.name), + })); + if (keys.length === 1) return suggestiveModeSettings; + return readPathValue(suggestiveModeSettings, keys.slice(1)); + } + + if (firstKey === "Relations") { + const relationsSettings = getLegacyRelationsSetting(); + if (keys.length === 1) return relationsSettings; + return readPathValue(relationsSettings, keys.slice(1)); + } + + return undefined; +}; + +const getLegacyQuerySettingByParentUid = (parentUid: string) => { + const scratchNode = getSubTree({ parentUid, key: "scratch" }); + const conditionsNode = getSubTree({ + tree: scratchNode.children, + key: "conditions", + }); + const selectionsNode = getSubTree({ + tree: scratchNode.children, + key: "selections", + }); + const customNode = getSubTree({ tree: scratchNode.children, key: "custom" }); + + return { + conditions: conditionsNode.children.map(roamNodeToCondition), + selections: selectionsNode.children.map((s) => ({ + text: s.text, + label: s.children?.[0]?.text || "", + })), + custom: customNode.children[0]?.text || "", + returnNode: "node", + }; +}; + +const getLegacyDiscourseNodeSetting = ( + nodeType: string, + keys: string[], +): unknown => { + let nodeUid = nodeType; + let tree = getBasicTreeByParentUid(nodeUid); + + if (tree.length === 0) { + const lookedUpUid = getPageUidByPageTitle( + `${DISCOURSE_NODE_PAGE_PREFIX}${nodeType}`, + ); + if (lookedUpUid) { + nodeUid = lookedUpUid; + tree = getBasicTreeByParentUid(nodeUid); + } + } + + if (tree.length === 0) return undefined; + + const canvasSettings = Object.fromEntries( + getSubTree({ tree, key: "canvas" }).children.map((c) => [ + c.text, + c.children[0]?.text || "", + ]), + ) as Record; + const attributes = Object.fromEntries( + getSubTree({ tree, key: "Attributes" }).children.map((c) => [ + c.text, + c.children[0]?.text || "", + ]), + ); + const overlayUid = getSubTree({ tree, key: "Overlay" }).uid; + const suggestiveRulesTree = getSubTree({ + tree, + key: "Suggestive Rules", + }).children; + const indexUid = getSubTree({ tree, key: "Index" }).uid; + const specificationUid = getSubTree({ tree, key: "Specification" }).uid; + const keyImageRaw = canvasSettings["key-image"]; + canvasSettings["color"] = (canvasSettings["color"] as string) || ""; + canvasSettings["alias"] = (canvasSettings["alias"] as string) || ""; + canvasSettings["key-image"] = keyImageRaw === "true"; + canvasSettings["key-image-option"] = + (canvasSettings["key-image-option"] as string) || "first-image"; + canvasSettings["query-builder-alias"] = + (canvasSettings["query-builder-alias"] as string) || ""; + + const legacySettings = { + type: nodeUid, + format: getSettingValueFromTree({ tree, key: "format" }), + shortcut: getSettingValueFromTree({ tree, key: "shortcut" }), + tag: getSettingValueFromTree({ tree, key: "tag" }), + graphOverview: tree.some((c) => c.text === "Graph Overview"), + description: getSettingValueFromTree({ tree, key: "description" }), + overlay: overlayUid + ? getBasicTreeByParentUid(overlayUid)[0]?.text || "" + : "", + attributes, + template: getSubTree({ tree, key: "template" }).children, + canvasSettings, + suggestiveRules: { + embeddingRef: + getSubTree({ tree: suggestiveRulesTree, key: "Embedding Block Ref" }) + .children[0]?.text || "", + isFirstChild: !!getSubTree({ + tree: suggestiveRulesTree, + key: "First Child", + }).uid, + }, + index: indexUid + ? getLegacyQuerySettingByParentUid(indexUid) + : DEFAULT_LEGACY_QUERY, + specification: { + enabled: specificationUid + ? !!getSubTree({ parentUid: specificationUid, key: "enabled" }).uid + : false, + query: specificationUid + ? getLegacyQuerySettingByParentUid(specificationUid) + : DEFAULT_LEGACY_QUERY, + }, + }; + + return readPathValue(legacySettings, keys); +}; + const getBlockPropsByUid = ( blockUid: string, keys: string[], @@ -312,12 +697,16 @@ export const getGlobalSettings = (): GlobalSettings => { export const getGlobalSetting = ( keys: string[], ): T | undefined => { - const settings = getGlobalSettings(); + if (!isNewSettingsStoreEnabled()) { + return getLegacyGlobalSetting(keys) as T | undefined; + } - return keys.reduce((current, key) => { - if (!isRecord(current) || !(key in current)) return undefined; - return current[key]; - }, settings) as T | undefined; + const settings = getGlobalSettings(); + const value = readPathValue(settings, keys) as T | undefined; + if (value === undefined) { + throw new Error(getMissingSettingError({ context: "Global", keys })); + } + return value; }; export const setGlobalSetting = (keys: string[], value: json): void => { @@ -368,12 +757,16 @@ export const getPersonalSettings = (): PersonalSettings => { export const getPersonalSetting = ( keys: string[], ): T | undefined => { - const settings = getPersonalSettings(); + if (!isNewSettingsStoreEnabled()) { + return getLegacyPersonalSetting(keys) as T | undefined; + } - return keys.reduce((current, key) => { - if (!isRecord(current) || !(key in current)) return undefined; - return current[key]; - }, settings) as T | undefined; + const settings = getPersonalSettings(); + const value = readPathValue(settings, keys) as T | undefined; + if (value === undefined) { + throw new Error(getMissingSettingError({ context: "Personal", keys })); + } + return value; }; export const setPersonalSetting = (keys: string[], value: json): void => { @@ -439,14 +832,23 @@ export const getDiscourseNodeSetting = ( nodeType: string, keys: string[], ): T | undefined => { - const settings = getDiscourseNodeSettings(nodeType); - - if (!settings) return undefined; + if (!isNewSettingsStoreEnabled()) { + return getLegacyDiscourseNodeSetting(nodeType, keys) as T | undefined; + } - return keys.reduce((current, key) => { - if (!isRecord(current) || !(key in current)) return undefined; - return current[key]; - }, settings) as T | undefined; + const settings = getDiscourseNodeSettings(nodeType); + const value = settings + ? (readPathValue(settings, keys) as T | undefined) + : undefined; + if (value === undefined) { + throw new Error( + getMissingSettingError({ + context: `Discourse Node (${nodeType})`, + keys, + }), + ); + } + return value; }; export const setDiscourseNodeSetting = ( @@ -540,3 +942,235 @@ export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => { return nodes; }; + +type DualReadProbeCase = { keys: string[] }; + +type DualReadProbeNodeCase = { + nodeType: string; + keys: string[]; +}; + +type DualReadProbeItem = { + scope: "personal" | "global" | "discourseNode"; + keys: string[]; + nodeType?: string; + legacyValue?: unknown; + blockPropsValue?: unknown; + legacyError?: string; + blockPropsError?: string; + match: boolean; +}; + +const normalizeProbeError = (error: unknown): string => { + return error instanceof Error ? error.message : String(error); +}; + +const safeSerialize = (value: unknown): string => { + try { + return JSON.stringify(value); + } catch { + return String(value); + } +}; + +const normalizeForComparison = (value: unknown): unknown => { + if (Array.isArray(value)) { + return value.map(normalizeForComparison); + } + if (isRecord(value)) { + return Object.keys(value) + .sort() + .reduce>((acc, key) => { + acc[key] = normalizeForComparison(value[key]); + return acc; + }, {}); + } + return value; +}; + +const runProbeRead = ( + reader: () => unknown, +): { + value?: unknown; + error?: string; +} => { + try { + return { value: reader() }; + } catch (error) { + return { error: normalizeProbeError(error) }; + } +}; + +const hasProbeMatch = ({ + legacyError, + blockPropsError, + legacyValue, + blockPropsValue, +}: { + legacyError?: string; + blockPropsError?: string; + legacyValue?: unknown; + blockPropsValue?: unknown; +}): boolean => { + if (legacyError || blockPropsError) return false; + return ( + safeSerialize(normalizeForComparison(legacyValue)) === + safeSerialize(normalizeForComparison(blockPropsValue)) + ); +}; + +const DEFAULT_DISCOURSE_NODE_PROBE_TYPE = "Claim"; +const DEFAULT_DISCOURSE_NODE_PROBE_KEYS: string[][] = [ + ["description"], + ["shortcut"], + ["tag"], + ["index"], + ["format"], + ["specification", "enabled"], + ["specification", "query"], + ["template"], + ["attributes"], + ["overlay"], + ["canvasSettings", "color"], + ["canvasSettings", "alias"], + ["canvasSettings", "key-image"], + ["canvasSettings", "key-image-option"], + ["canvasSettings", "query-builder-alias"], + ["graphOverview"], + ["suggestiveRules", "embeddingRef"], + ["suggestiveRules", "isFirstChild"], +]; + +export const runDualReadProbe = ({ + personalCases = [ + { keys: ["Discourse context overlay"] }, + { keys: ["Suggestive mode overlay"] }, + { keys: ["Text selection popup"] }, + { keys: ["Disable sidebar open"] }, + { keys: ["Page preview"] }, + { keys: ["Hide feedback button"] }, + { keys: ["Auto canvas relations"] }, + { keys: ["Overlay in canvas"] }, + { keys: ["Streamline styling"] }, + { keys: ["Disable product diagnostics"] }, + { keys: ["Discourse tool shortcut"] }, + { keys: ["Personal node menu trigger"] }, + { keys: ["Node search menu trigger"] }, + { keys: ["Query"] }, + { keys: ["Query", "Hide query metadata"] }, + { keys: ["Query", "Default page size"] }, + { keys: ["Query", "Query pages"] }, + { keys: ["Query", "Default filters"] }, + { keys: ["Left sidebar"] }, + ], + globalCases = [ + { keys: ["Trigger"] }, + { keys: ["Canvas page format"] }, + { keys: ["Left sidebar"] }, + { keys: ["Left sidebar", "Children"] }, + { keys: ["Left sidebar", "Settings", "Collapsable"] }, + { keys: ["Left sidebar", "Settings", "Folded"] }, + { keys: ["Export"] }, + { keys: ["Export", "Remove special characters"] }, + { keys: ["Export", "Resolve block references"] }, + { keys: ["Export", "Resolve block embeds"] }, + { keys: ["Export", "Append referenced node"] }, + { keys: ["Export", "Link type"] }, + { keys: ["Export", "Max filename length"] }, + { keys: ["Export", "Frontmatter"] }, + { keys: ["Suggestive mode"] }, + { keys: ["Suggestive mode", "Include current page relations"] }, + { keys: ["Suggestive mode", "Include parent and child blocks"] }, + { keys: ["Suggestive mode", "Page groups"] }, + { keys: ["Relations"] }, + ], + discourseNodeCases = DEFAULT_DISCOURSE_NODE_PROBE_KEYS.map((keys) => ({ + nodeType: DEFAULT_DISCOURSE_NODE_PROBE_TYPE, + keys, + })), +}: { + personalCases?: DualReadProbeCase[]; + globalCases?: DualReadProbeCase[]; + discourseNodeCases?: DualReadProbeNodeCase[]; +} = {}) => { + const rows: DualReadProbeItem[] = []; + + personalCases.forEach(({ keys }) => { + const legacy = runProbeRead(() => getLegacyPersonalSetting(keys)); + const blockProps = runProbeRead(() => + readPathValue(getPersonalSettings(), keys), + ); + rows.push({ + scope: "personal", + keys, + legacyValue: legacy.value, + blockPropsValue: blockProps.value, + legacyError: legacy.error, + blockPropsError: blockProps.error, + match: hasProbeMatch({ + legacyError: legacy.error, + blockPropsError: blockProps.error, + legacyValue: legacy.value, + blockPropsValue: blockProps.value, + }), + }); + }); + + globalCases.forEach(({ keys }) => { + const legacy = runProbeRead(() => getLegacyGlobalSetting(keys)); + const blockProps = runProbeRead(() => + readPathValue(getGlobalSettings(), keys), + ); + rows.push({ + scope: "global", + keys, + legacyValue: legacy.value, + blockPropsValue: blockProps.value, + legacyError: legacy.error, + blockPropsError: blockProps.error, + match: hasProbeMatch({ + legacyError: legacy.error, + blockPropsError: blockProps.error, + legacyValue: legacy.value, + blockPropsValue: blockProps.value, + }), + }); + }); + + discourseNodeCases.forEach(({ nodeType, keys }) => { + const legacy = runProbeRead(() => + getLegacyDiscourseNodeSetting(nodeType, keys), + ); + const blockProps = runProbeRead(() => + readPathValue(getDiscourseNodeSettings(nodeType), keys), + ); + rows.push({ + scope: "discourseNode", + nodeType, + keys, + legacyValue: legacy.value, + blockPropsValue: blockProps.value, + legacyError: legacy.error, + blockPropsError: blockProps.error, + match: hasProbeMatch({ + legacyError: legacy.error, + blockPropsError: blockProps.error, + legacyValue: legacy.value, + blockPropsValue: blockProps.value, + }), + }); + }); + + const mismatches = rows.filter((row) => !row.match); + + return { + newSettingsStoreEnabled: isNewSettingsStoreEnabled(), + summary: { + total: rows.length, + matches: rows.filter((row) => row.match).length, + mismatches: mismatches.length, + }, + rows, + mismatches, + }; +}; diff --git a/apps/roam/src/components/settings/utils/zodSchema.example.ts b/apps/roam/src/components/settings/utils/zodSchema.example.ts index 2e6bfa960..7a9041666 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.example.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.example.ts @@ -404,9 +404,9 @@ const defaultPersonalSettings: PersonalSettings = { "Auto canvas relations": false, "Disable product diagnostics": false, Query: { - "Hide query metadata": false, + "Hide query metadata": true, "Default page size": 10, - "Query pages": [], + "Query pages": ["discourse-graph/queries/*"], "Default filters": {}, }, }; diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 8c8486ed7..f34d7f5f7 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -95,6 +95,13 @@ const booleanWithDefault = (defaultVal: boolean) => .optional() .transform((val) => val ?? defaultVal); +const defaultNodeIndexValue = { + conditions: [], + selections: [], + custom: "", + returnNode: "node", +}; + export const DiscourseNodeSchema = z.object({ text: z.string(), type: z.string(), @@ -109,13 +116,7 @@ export const DiscourseNodeSchema = z.object({ }) .nullable() .optional() - .transform( - (val) => - val ?? { - enabled: false, - query: { conditions: [], selections: [], custom: "", returnNode: "" }, - }, - ), + .transform((val) => val ?? { enabled: false, query: defaultNodeIndexValue }), template: z .array(RoamNodeSchema) .nullable() @@ -129,7 +130,7 @@ export const DiscourseNodeSchema = z.object({ .optional() .transform((val) => val ?? {}), overlay: stringWithDefault(""), - index: IndexSchema.nullable().optional(), + index: IndexSchema.nullable().optional().transform((val) => val ?? defaultNodeIndexValue), suggestiveRules: SuggestiveRulesSchema.default({}), backedBy: z .enum(["user", "default", "relation"]) @@ -230,9 +231,9 @@ export const StoredFiltersSchema = z.object({ }); export const QuerySettingsSchema = z.object({ - "Hide query metadata": z.boolean().default(false), + "Hide query metadata": z.boolean().default(true), "Default page size": z.number().default(10), - "Query pages": z.array(z.string()).default([]), + "Query pages": z.array(z.string()).default(["discourse-graph/queries/*"]), "Default filters": z.record(z.string(), StoredFiltersSchema).default({}), }); diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 0579784c3..187417090 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -42,6 +42,8 @@ import { DISALLOW_DIAGNOSTICS, } from "./data/userSettings"; import { initSchema } from "./components/settings/utils/init"; +import { runDualReadProbe } from "./components/settings/utils/accessors"; + export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; export default runExtension(async (onloadArgs) => { @@ -123,6 +125,10 @@ export default runExtension(async (onloadArgs) => { } const { extensionAPI } = onloadArgs; + const debugWindow = window as unknown as { [key: string]: unknown }; + debugWindow["__DG_DEBUG__"] = { + runDualReadProbe, + }; window.roamjs.extension.queryBuilder = { runQuery: (parentUid: string) => runQuery({ parentUid, extensionAPI }).then( From 29263ac6c8ebeb16c141183773372f8bc6ce2d33 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 23 Feb 2026 01:05:56 +0530 Subject: [PATCH 02/10] remove console.logs --- .../components/settings/utils/accessors.ts | 232 ------------------ apps/roam/src/index.ts | 5 - 2 files changed, 237 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index f0933ffe6..a579325b9 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -942,235 +942,3 @@ export const getAllDiscourseNodes = (): DiscourseNodeSettings[] => { return nodes; }; - -type DualReadProbeCase = { keys: string[] }; - -type DualReadProbeNodeCase = { - nodeType: string; - keys: string[]; -}; - -type DualReadProbeItem = { - scope: "personal" | "global" | "discourseNode"; - keys: string[]; - nodeType?: string; - legacyValue?: unknown; - blockPropsValue?: unknown; - legacyError?: string; - blockPropsError?: string; - match: boolean; -}; - -const normalizeProbeError = (error: unknown): string => { - return error instanceof Error ? error.message : String(error); -}; - -const safeSerialize = (value: unknown): string => { - try { - return JSON.stringify(value); - } catch { - return String(value); - } -}; - -const normalizeForComparison = (value: unknown): unknown => { - if (Array.isArray(value)) { - return value.map(normalizeForComparison); - } - if (isRecord(value)) { - return Object.keys(value) - .sort() - .reduce>((acc, key) => { - acc[key] = normalizeForComparison(value[key]); - return acc; - }, {}); - } - return value; -}; - -const runProbeRead = ( - reader: () => unknown, -): { - value?: unknown; - error?: string; -} => { - try { - return { value: reader() }; - } catch (error) { - return { error: normalizeProbeError(error) }; - } -}; - -const hasProbeMatch = ({ - legacyError, - blockPropsError, - legacyValue, - blockPropsValue, -}: { - legacyError?: string; - blockPropsError?: string; - legacyValue?: unknown; - blockPropsValue?: unknown; -}): boolean => { - if (legacyError || blockPropsError) return false; - return ( - safeSerialize(normalizeForComparison(legacyValue)) === - safeSerialize(normalizeForComparison(blockPropsValue)) - ); -}; - -const DEFAULT_DISCOURSE_NODE_PROBE_TYPE = "Claim"; -const DEFAULT_DISCOURSE_NODE_PROBE_KEYS: string[][] = [ - ["description"], - ["shortcut"], - ["tag"], - ["index"], - ["format"], - ["specification", "enabled"], - ["specification", "query"], - ["template"], - ["attributes"], - ["overlay"], - ["canvasSettings", "color"], - ["canvasSettings", "alias"], - ["canvasSettings", "key-image"], - ["canvasSettings", "key-image-option"], - ["canvasSettings", "query-builder-alias"], - ["graphOverview"], - ["suggestiveRules", "embeddingRef"], - ["suggestiveRules", "isFirstChild"], -]; - -export const runDualReadProbe = ({ - personalCases = [ - { keys: ["Discourse context overlay"] }, - { keys: ["Suggestive mode overlay"] }, - { keys: ["Text selection popup"] }, - { keys: ["Disable sidebar open"] }, - { keys: ["Page preview"] }, - { keys: ["Hide feedback button"] }, - { keys: ["Auto canvas relations"] }, - { keys: ["Overlay in canvas"] }, - { keys: ["Streamline styling"] }, - { keys: ["Disable product diagnostics"] }, - { keys: ["Discourse tool shortcut"] }, - { keys: ["Personal node menu trigger"] }, - { keys: ["Node search menu trigger"] }, - { keys: ["Query"] }, - { keys: ["Query", "Hide query metadata"] }, - { keys: ["Query", "Default page size"] }, - { keys: ["Query", "Query pages"] }, - { keys: ["Query", "Default filters"] }, - { keys: ["Left sidebar"] }, - ], - globalCases = [ - { keys: ["Trigger"] }, - { keys: ["Canvas page format"] }, - { keys: ["Left sidebar"] }, - { keys: ["Left sidebar", "Children"] }, - { keys: ["Left sidebar", "Settings", "Collapsable"] }, - { keys: ["Left sidebar", "Settings", "Folded"] }, - { keys: ["Export"] }, - { keys: ["Export", "Remove special characters"] }, - { keys: ["Export", "Resolve block references"] }, - { keys: ["Export", "Resolve block embeds"] }, - { keys: ["Export", "Append referenced node"] }, - { keys: ["Export", "Link type"] }, - { keys: ["Export", "Max filename length"] }, - { keys: ["Export", "Frontmatter"] }, - { keys: ["Suggestive mode"] }, - { keys: ["Suggestive mode", "Include current page relations"] }, - { keys: ["Suggestive mode", "Include parent and child blocks"] }, - { keys: ["Suggestive mode", "Page groups"] }, - { keys: ["Relations"] }, - ], - discourseNodeCases = DEFAULT_DISCOURSE_NODE_PROBE_KEYS.map((keys) => ({ - nodeType: DEFAULT_DISCOURSE_NODE_PROBE_TYPE, - keys, - })), -}: { - personalCases?: DualReadProbeCase[]; - globalCases?: DualReadProbeCase[]; - discourseNodeCases?: DualReadProbeNodeCase[]; -} = {}) => { - const rows: DualReadProbeItem[] = []; - - personalCases.forEach(({ keys }) => { - const legacy = runProbeRead(() => getLegacyPersonalSetting(keys)); - const blockProps = runProbeRead(() => - readPathValue(getPersonalSettings(), keys), - ); - rows.push({ - scope: "personal", - keys, - legacyValue: legacy.value, - blockPropsValue: blockProps.value, - legacyError: legacy.error, - blockPropsError: blockProps.error, - match: hasProbeMatch({ - legacyError: legacy.error, - blockPropsError: blockProps.error, - legacyValue: legacy.value, - blockPropsValue: blockProps.value, - }), - }); - }); - - globalCases.forEach(({ keys }) => { - const legacy = runProbeRead(() => getLegacyGlobalSetting(keys)); - const blockProps = runProbeRead(() => - readPathValue(getGlobalSettings(), keys), - ); - rows.push({ - scope: "global", - keys, - legacyValue: legacy.value, - blockPropsValue: blockProps.value, - legacyError: legacy.error, - blockPropsError: blockProps.error, - match: hasProbeMatch({ - legacyError: legacy.error, - blockPropsError: blockProps.error, - legacyValue: legacy.value, - blockPropsValue: blockProps.value, - }), - }); - }); - - discourseNodeCases.forEach(({ nodeType, keys }) => { - const legacy = runProbeRead(() => - getLegacyDiscourseNodeSetting(nodeType, keys), - ); - const blockProps = runProbeRead(() => - readPathValue(getDiscourseNodeSettings(nodeType), keys), - ); - rows.push({ - scope: "discourseNode", - nodeType, - keys, - legacyValue: legacy.value, - blockPropsValue: blockProps.value, - legacyError: legacy.error, - blockPropsError: blockProps.error, - match: hasProbeMatch({ - legacyError: legacy.error, - blockPropsError: blockProps.error, - legacyValue: legacy.value, - blockPropsValue: blockProps.value, - }), - }); - }); - - const mismatches = rows.filter((row) => !row.match); - - return { - newSettingsStoreEnabled: isNewSettingsStoreEnabled(), - summary: { - total: rows.length, - matches: rows.filter((row) => row.match).length, - mismatches: mismatches.length, - }, - rows, - mismatches, - }; -}; diff --git a/apps/roam/src/index.ts b/apps/roam/src/index.ts index 187417090..9eb8095aa 100644 --- a/apps/roam/src/index.ts +++ b/apps/roam/src/index.ts @@ -42,7 +42,6 @@ import { DISALLOW_DIAGNOSTICS, } from "./data/userSettings"; import { initSchema } from "./components/settings/utils/init"; -import { runDualReadProbe } from "./components/settings/utils/accessors"; export const DEFAULT_CANVAS_PAGE_FORMAT = "Canvas/*"; @@ -125,10 +124,6 @@ export default runExtension(async (onloadArgs) => { } const { extensionAPI } = onloadArgs; - const debugWindow = window as unknown as { [key: string]: unknown }; - debugWindow["__DG_DEBUG__"] = { - runDualReadProbe, - }; window.roamjs.extension.queryBuilder = { runQuery: (parentUid: string) => runQuery({ parentUid, extensionAPI }).then( From 4aec399e4ed5e650a18a088bb30f4d1bfaa7e556 Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 23 Feb 2026 01:10:05 +0530 Subject: [PATCH 03/10] address review --- .../components/settings/utils/zodSchema.ts | 22 ++++++++++++++----- 1 file changed, 16 insertions(+), 6 deletions(-) diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index f34d7f5f7..92d991f73 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -95,12 +95,18 @@ const booleanWithDefault = (defaultVal: boolean) => .optional() .transform((val) => val ?? defaultVal); -const defaultNodeIndexValue = { - conditions: [], - selections: [], +const defaultNodeIndex = () => ({ + conditions: [] as { + type: string; + relation: string; + source: string; + uid: string; + not: boolean; + }[], + selections: [] as { text: string; label: string }[], custom: "", returnNode: "node", -}; +}); export const DiscourseNodeSchema = z.object({ text: z.string(), @@ -116,7 +122,9 @@ export const DiscourseNodeSchema = z.object({ }) .nullable() .optional() - .transform((val) => val ?? { enabled: false, query: defaultNodeIndexValue }), + .transform( + (val) => val ?? { enabled: false, query: defaultNodeIndex() }, + ), template: z .array(RoamNodeSchema) .nullable() @@ -130,7 +138,9 @@ export const DiscourseNodeSchema = z.object({ .optional() .transform((val) => val ?? {}), overlay: stringWithDefault(""), - index: IndexSchema.nullable().optional().transform((val) => val ?? defaultNodeIndexValue), + index: IndexSchema.nullable() + .optional() + .transform((val) => val ?? defaultNodeIndex()), suggestiveRules: SuggestiveRulesSchema.default({}), backedBy: z .enum(["user", "default", "relation"]) From e36e3fed18b2bdaf5c0d9bb67664b4d179cf7a6d Mon Sep 17 00:00:00 2001 From: sid597 Date: Mon, 23 Feb 2026 01:17:02 +0530 Subject: [PATCH 04/10] prettier --- apps/roam/src/components/settings/utils/zodSchema.ts | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/apps/roam/src/components/settings/utils/zodSchema.ts b/apps/roam/src/components/settings/utils/zodSchema.ts index 92d991f73..212c09e21 100644 --- a/apps/roam/src/components/settings/utils/zodSchema.ts +++ b/apps/roam/src/components/settings/utils/zodSchema.ts @@ -122,9 +122,7 @@ export const DiscourseNodeSchema = z.object({ }) .nullable() .optional() - .transform( - (val) => val ?? { enabled: false, query: defaultNodeIndex() }, - ), + .transform((val) => val ?? { enabled: false, query: defaultNodeIndex() }), template: z .array(RoamNodeSchema) .nullable() From 6b5d6230a172dd264c511ffd2cc5c70b86c58b07 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 24 Feb 2026 20:08:14 +0530 Subject: [PATCH 05/10] add comment describing why we do it like this --- apps/roam/src/components/settings/utils/accessors.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index a579325b9..7ff7291b6 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -262,6 +262,11 @@ const getLegacyPersonalSetting = (keys: string[]): unknown => { return undefined; }; +// NOTE(ENG-1469): This returns the block props schema shape (Record). Runtime consumers use getDiscourseRelations() +// which returns a flat DiscourseRelation[] with a different structure (one entry per +// if-block, triples at top level, no nodePositions). When migrating getDiscourseRelations() +// to read from block props, it will need a conversion from this shape to the flat array. const getLegacyRelationsSetting = (): Record => { const settingsUid = getPageUidByPageTitle(DG_BLOCK_PROP_SETTINGS_PAGE_TITLE); if (!settingsUid) return DEFAULT_GLOBAL_SETTINGS.Relations; From ed2fe4dbe5a7c4d9fac5ebe5a80535b871e1b8e8 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 24 Feb 2026 22:23:27 +0530 Subject: [PATCH 06/10] reviewed --- .../components/settings/utils/accessors.ts | 48 +++++++++---------- 1 file changed, 23 insertions(+), 25 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 7ff7291b6..19e81caae 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -174,7 +174,7 @@ const DEFAULT_LEGACY_QUERY = { returnNode: "node", }; -const PERSONAL_OLD_KEY_MAP = new Map([ +const PERSONAL_SCHEMA_PATH_TO_LEGACY_KEY = new Map([ [pathKey(["Discourse context overlay"]), "discourse-context-overlay"], [pathKey(["Suggestive mode overlay"]), "suggestive-mode-overlay"], [pathKey(["Text selection popup"]), "text-selection-popup"], @@ -194,10 +194,6 @@ const PERSONAL_OLD_KEY_MAP = new Map([ [pathKey(["Query", "Default filters"]), "default-filters"], ]); -const PERSONAL_OLD_DEFAULT_OVERRIDES = new Map([ - [pathKey(["Query", "Hide query metadata"]), true], -]); - const getLegacyPersonalLeftSidebarSetting = (): Record => { const settings = getFormattedConfigTree(); @@ -221,14 +217,13 @@ const getLegacyPersonalLeftSidebarSetting = (): Record => { }; const getLegacyPersonalSetting = (keys: string[]): unknown => { - const mappedOldKey = PERSONAL_OLD_KEY_MAP.get(pathKey(keys)); + if (keys.length === 0) return undefined; + + const mappedOldKey = PERSONAL_SCHEMA_PATH_TO_LEGACY_KEY.get(pathKey(keys)); if (mappedOldKey) { - const path = pathKey(keys); return getSetting( mappedOldKey, - PERSONAL_OLD_DEFAULT_OVERRIDES.has(path) - ? PERSONAL_OLD_DEFAULT_OVERRIDES.get(path) - : readPathValue(DEFAULT_PERSONAL_SETTINGS, keys), + readPathValue(DEFAULT_PERSONAL_SETTINGS, keys), ); } @@ -284,10 +279,10 @@ const getLegacyRelationsSetting = (): Record => { return Object.fromEntries( relationNodes.map((relationNode) => { - const relationTree = relationNode.children || []; + const relationTree = relationNode.children; const ifBlocks = getSubTree({ tree: relationTree, key: "If" }).children; const ifConditions = ifBlocks.map((ifBlock) => { - const blockChildren = ifBlock.children || []; + const blockChildren = ifBlock.children; const nodePositionsNode = blockChildren.find((c) => /node positions/i.test(c.text), ); @@ -297,14 +292,14 @@ const getLegacyRelationsSetting = (): Record => { (c) => [ c.text, - c.children?.[0]?.text || "", - c.children?.[0]?.children?.[0]?.text || "", + c.children[0]?.text || "", + c.children[0]?.children[0]?.text || "", ] as [string, string, string], ); const nodePositions = Object.fromEntries( (nodePositionsNode?.children || []).map((c) => [ c.text, - c.children?.[0]?.text || "", + c.children[0]?.text || "", ]), ); return { triples, nodePositions }; @@ -333,6 +328,7 @@ const getLegacyRelationsSetting = (): Record => { ); }; +// Reconstructs global settings from getFormattedConfigTree() shape to match block-props schema shape const getLegacyGlobalSetting = (keys: string[]): unknown => { if (keys.length === 0) return undefined; @@ -447,6 +443,7 @@ const getLegacyQuerySettingByParentUid = (parentUid: string) => { }; }; +// Reconstructs per-node settings from Roam tree structure to match block-props schema shape const getLegacyDiscourseNodeSetting = ( nodeType: string, keys: string[], @@ -466,12 +463,21 @@ const getLegacyDiscourseNodeSetting = ( if (tree.length === 0) return undefined; - const canvasSettings = Object.fromEntries( + const rawCanvas = Object.fromEntries( getSubTree({ tree, key: "canvas" }).children.map((c) => [ c.text, c.children[0]?.text || "", ]), - ) as Record; + ); + /* eslint-disable @typescript-eslint/naming-convention */ + const canvasSettings = { + color: rawCanvas["color"] || "", + alias: rawCanvas["alias"] || "", + "key-image": rawCanvas["key-image"] === "true", + "key-image-option": rawCanvas["key-image-option"] || "first-image", + "query-builder-alias": rawCanvas["query-builder-alias"] || "", + }; + /* eslint-enable @typescript-eslint/naming-convention */ const attributes = Object.fromEntries( getSubTree({ tree, key: "Attributes" }).children.map((c) => [ c.text, @@ -485,14 +491,6 @@ const getLegacyDiscourseNodeSetting = ( }).children; const indexUid = getSubTree({ tree, key: "Index" }).uid; const specificationUid = getSubTree({ tree, key: "Specification" }).uid; - const keyImageRaw = canvasSettings["key-image"]; - canvasSettings["color"] = (canvasSettings["color"] as string) || ""; - canvasSettings["alias"] = (canvasSettings["alias"] as string) || ""; - canvasSettings["key-image"] = keyImageRaw === "true"; - canvasSettings["key-image-option"] = - (canvasSettings["key-image-option"] as string) || "first-image"; - canvasSettings["query-builder-alias"] = - (canvasSettings["query-builder-alias"] as string) || ""; const legacySettings = { type: nodeUid, From d2c821ff5230dbcbf86f21f370e9f7bf883155d7 Mon Sep 17 00:00:00 2001 From: sid597 Date: Tue, 24 Feb 2026 22:45:09 +0530 Subject: [PATCH 07/10] rebase, review --- .../components/settings/utils/accessors.ts | 30 ++++++++----------- 1 file changed, 13 insertions(+), 17 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 19e81caae..292a7f322 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -194,25 +194,21 @@ const PERSONAL_SCHEMA_PATH_TO_LEGACY_KEY = new Map([ [pathKey(["Query", "Default filters"]), "default-filters"], ]); -const getLegacyPersonalLeftSidebarSetting = (): Record => { +const getLegacyPersonalLeftSidebarSetting = (): unknown[] => { const settings = getFormattedConfigTree(); /* eslint-disable @typescript-eslint/naming-convention */ - return Object.fromEntries( - settings.leftSidebar.personal.sections.map((section) => [ - section.text, - { - Children: (section.children || []).map((child) => ({ - Page: child.text, - Alias: child.alias?.value || "", - })), - Settings: { - "Truncate-result?": section.settings?.truncateResult.value ?? 75, - Folded: section.settings?.folded.value ?? false, - }, - }, - ]), - ); + return settings.leftSidebar.personal.sections.map((section) => ({ + name: section.text, + Children: (section.children || []).map((child) => ({ + uid: child.uid, + Alias: child.alias?.value || "", + })), + Settings: { + "Truncate-result?": section.settings?.truncateResult.value ?? 75, + Folded: section.settings?.folded.value ?? false, + }, + })); /* eslint-enable @typescript-eslint/naming-convention */ }; @@ -436,7 +432,7 @@ const getLegacyQuerySettingByParentUid = (parentUid: string) => { conditions: conditionsNode.children.map(roamNodeToCondition), selections: selectionsNode.children.map((s) => ({ text: s.text, - label: s.children?.[0]?.text || "", + label: s.children[0]?.text || "", })), custom: customNode.children[0]?.text || "", returnNode: "node", From 847359fa97e02c8af0ce8addeaa0ca7df978f0c5 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 25 Feb 2026 01:31:07 +0530 Subject: [PATCH 08/10] address review --- apps/roam/src/components/settings/utils/accessors.ts | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 292a7f322..092364ca0 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -114,6 +114,12 @@ const formatSettingPath = (keys: string[]): string => const readPathValue = (root: unknown, keys: string[]): unknown => keys.reduce((current, key) => { + if (Array.isArray(current)) { + const index = Number(key); + return Number.isInteger(index) && index >= 0 && index < current.length + ? current[index] + : undefined; + } if (!isRecord(current) || !(key in current)) return undefined; return current[key]; }, root); From 0d6ee2bdc766784c6c9dac8e4a8442184abc1817 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 25 Feb 2026 17:09:48 +0530 Subject: [PATCH 09/10] explicit throw, now zodschema default reads --- .../components/settings/utils/accessors.ts | 73 +++++++++++++------ 1 file changed, 49 insertions(+), 24 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 092364ca0..0da254a80 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -706,12 +706,19 @@ export const getGlobalSetting = ( return getLegacyGlobalSetting(keys) as T | undefined; } - const settings = getGlobalSettings(); - const value = readPathValue(settings, keys) as T | undefined; - if (value === undefined) { - throw new Error(getMissingSettingError({ context: "Global", keys })); + const { blockProps } = getBlockPropBasedSettings({ + keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global], + }); + const rawValue = readPathValue(blockProps || {}, keys); + if (rawValue === undefined) { + const legacyValue = getLegacyGlobalSetting(keys); + if (legacyValue !== undefined) { + throw new Error(getMissingSettingError({ context: "Global", keys })); + } } - return value; + + const settings = GlobalSettingsSchema.parse(blockProps || {}); + return readPathValue(settings, keys) as T; }; export const setGlobalSetting = (keys: string[], value: json): void => { @@ -766,12 +773,20 @@ export const getPersonalSetting = ( return getLegacyPersonalSetting(keys) as T | undefined; } - const settings = getPersonalSettings(); - const value = readPathValue(settings, keys) as T | undefined; - if (value === undefined) { - throw new Error(getMissingSettingError({ context: "Personal", keys })); + const personalKey = getPersonalSettingsKey(); + const { blockProps } = getBlockPropBasedSettings({ + keys: [personalKey], + }); + const rawValue = readPathValue(blockProps || {}, keys); + if (rawValue === undefined) { + const legacyValue = getLegacyPersonalSetting(keys); + if (legacyValue !== undefined) { + throw new Error(getMissingSettingError({ context: "Personal", keys })); + } } - return value; + + const settings = PersonalSettingsSchema.parse(blockProps || {}); + return readPathValue(settings, keys) as T; }; export const setPersonalSetting = (keys: string[], value: json): void => { @@ -802,9 +817,7 @@ export const setPersonalSetting = (keys: string[], value: json): void => { }); }; -export const getDiscourseNodeSettings = ( - nodeType: string, -): DiscourseNodeSettings | undefined => { +const getRawDiscourseNodeBlockProps = (nodeType: string): json | undefined => { let pageUid = nodeType; let blockProps = getBlockPropsByUid(pageUid, []); @@ -818,6 +831,13 @@ export const getDiscourseNodeSettings = ( } } + return blockProps; +}; + +export const getDiscourseNodeSettings = ( + nodeType: string, +): DiscourseNodeSettings | undefined => { + const blockProps = getRawDiscourseNodeBlockProps(nodeType); if (!blockProps) return undefined; const result = DiscourseNodeSchema.safeParse(blockProps); @@ -841,19 +861,24 @@ export const getDiscourseNodeSetting = ( return getLegacyDiscourseNodeSetting(nodeType, keys) as T | undefined; } - const settings = getDiscourseNodeSettings(nodeType); - const value = settings - ? (readPathValue(settings, keys) as T | undefined) + const rawBlockProps = getRawDiscourseNodeBlockProps(nodeType); + const rawValue = rawBlockProps + ? readPathValue(rawBlockProps, keys) : undefined; - if (value === undefined) { - throw new Error( - getMissingSettingError({ - context: `Discourse Node (${nodeType})`, - keys, - }), - ); + if (rawValue === undefined) { + const legacyValue = getLegacyDiscourseNodeSetting(nodeType, keys); + if (legacyValue !== undefined) { + throw new Error( + getMissingSettingError({ + context: `Discourse Node (${nodeType})`, + keys, + }), + ); + } } - return value; + + const settings = getDiscourseNodeSettings(nodeType); + return settings ? (readPathValue(settings, keys) as T) : undefined; }; export const setDiscourseNodeSetting = ( From 9b10a40fd84f2ddb61a2ddee04f919bc846c4105 Mon Sep 17 00:00:00 2001 From: sid597 Date: Wed, 25 Feb 2026 21:36:47 +0530 Subject: [PATCH 10/10] replace throw with warn, throw was too strong, with warn we test if legacy and block props are same --- .../components/settings/utils/accessors.ts | 78 +++++++------------ 1 file changed, 27 insertions(+), 51 deletions(-) diff --git a/apps/roam/src/components/settings/utils/accessors.ts b/apps/roam/src/components/settings/utils/accessors.ts index 0da254a80..ed5b116d3 100644 --- a/apps/roam/src/components/settings/utils/accessors.ts +++ b/apps/roam/src/components/settings/utils/accessors.ts @@ -126,15 +126,6 @@ const readPathValue = (root: unknown, keys: string[]): unknown => const pathKey = (keys: string[]): string => keys.join("::"); -const getMissingSettingError = ({ - context, - keys, -}: { - context: string; - keys: string[]; -}): string => - `[DG Accessor] Missing ${context} setting at path: ${formatSettingPath(keys)} (dual-read ON)`; - const validateSettingValue = ({ schema, keys, @@ -706,19 +697,16 @@ export const getGlobalSetting = ( return getLegacyGlobalSetting(keys) as T | undefined; } - const { blockProps } = getBlockPropBasedSettings({ - keys: [TOP_LEVEL_BLOCK_PROP_KEYS.global], - }); - const rawValue = readPathValue(blockProps || {}, keys); - if (rawValue === undefined) { - const legacyValue = getLegacyGlobalSetting(keys); - if (legacyValue !== undefined) { - throw new Error(getMissingSettingError({ context: "Global", keys })); - } + const settings = getGlobalSettings(); + const blockPropsValue = readPathValue(settings, keys); + const legacyValue = getLegacyGlobalSetting(keys); + if (JSON.stringify(blockPropsValue) !== JSON.stringify(legacyValue)) { + console.warn( + `[DG Dual-Read] Mismatch at Global > ${formatSettingPath(keys)}`, + { blockProps: blockPropsValue, legacy: legacyValue }, + ); } - - const settings = GlobalSettingsSchema.parse(blockProps || {}); - return readPathValue(settings, keys) as T; + return blockPropsValue as T | undefined; }; export const setGlobalSetting = (keys: string[], value: json): void => { @@ -773,20 +761,16 @@ export const getPersonalSetting = ( return getLegacyPersonalSetting(keys) as T | undefined; } - const personalKey = getPersonalSettingsKey(); - const { blockProps } = getBlockPropBasedSettings({ - keys: [personalKey], - }); - const rawValue = readPathValue(blockProps || {}, keys); - if (rawValue === undefined) { - const legacyValue = getLegacyPersonalSetting(keys); - if (legacyValue !== undefined) { - throw new Error(getMissingSettingError({ context: "Personal", keys })); - } + const settings = getPersonalSettings(); + const blockPropsValue = readPathValue(settings, keys); + const legacyValue = getLegacyPersonalSetting(keys); + if (JSON.stringify(blockPropsValue) !== JSON.stringify(legacyValue)) { + console.warn( + `[DG Dual-Read] Mismatch at Personal > ${formatSettingPath(keys)}`, + { blockProps: blockPropsValue, legacy: legacyValue }, + ); } - - const settings = PersonalSettingsSchema.parse(blockProps || {}); - return readPathValue(settings, keys) as T; + return blockPropsValue as T | undefined; }; export const setPersonalSetting = (keys: string[], value: json): void => { @@ -861,24 +845,16 @@ export const getDiscourseNodeSetting = ( return getLegacyDiscourseNodeSetting(nodeType, keys) as T | undefined; } - const rawBlockProps = getRawDiscourseNodeBlockProps(nodeType); - const rawValue = rawBlockProps - ? readPathValue(rawBlockProps, keys) - : undefined; - if (rawValue === undefined) { - const legacyValue = getLegacyDiscourseNodeSetting(nodeType, keys); - if (legacyValue !== undefined) { - throw new Error( - getMissingSettingError({ - context: `Discourse Node (${nodeType})`, - keys, - }), - ); - } - } - const settings = getDiscourseNodeSettings(nodeType); - return settings ? (readPathValue(settings, keys) as T) : undefined; + const blockPropsValue = settings ? readPathValue(settings, keys) : undefined; + const legacyValue = getLegacyDiscourseNodeSetting(nodeType, keys); + if (JSON.stringify(blockPropsValue) !== JSON.stringify(legacyValue)) { + console.warn( + `[DG Dual-Read] Mismatch at Discourse Node (${nodeType}) > ${formatSettingPath(keys)}`, + { blockProps: blockPropsValue, legacy: legacyValue }, + ); + } + return blockPropsValue as T | undefined; }; export const setDiscourseNodeSetting = (