Skip to content

ENG-1975 Define schema file contract and shared foundation for Obsidian export/import - #1180

Open
trangdoan982 wants to merge 4 commits into
mainfrom
eng-1975-schema-foundation
Open

ENG-1975 Define schema file contract and shared foundation for Obsidian export/import#1180
trangdoan982 wants to merge 4 commits into
mainfrom
eng-1975-schema-foundation

Conversation

@trangdoan982

@trangdoan982 trangdoan982 commented Jun 30, 2026

Copy link
Copy Markdown
Member

Summary

Ships the shared foundation that the schema selection panel, export, and import PRs all stack on top of.

  • types.ts — adds DiscourseSchemaFile and DiscourseSchemaTemplate type definitions for the exported JSON file format
  • specValidation.ts — adds getDgSchemaFileName(vaultName?) (kebab-cases vault name → dg-schema-<vault>.json) and DG_SCHEMA_EXPORT_VERSION = 1
  • ReactRootModal.tsx — abstract Obsidian Modal subclass that mounts a React root into contentEl and unmounts on close; DRYs the createRoot/unmount boilerplate that was previously copy-pasted across 4 existing modals
  • useSchemaSelection.ts — hook that initializes from plugin settings (all items selected by default), tracks per-category selection state, and enforces dependency constraints (selecting a triple auto-selects its required node/relation types)

Stack

This is PR 1 of 5 for FEE-840. Stack order:

  1. This PR — shared foundation (ENG-1975)
  2. Schema selection panel UI (ENG-2083)
  3. Schema export command (ENG-1976)
  4. Schema import data layer (ENG-1977)
  5. Schema import UI (ENG-2084)

Test plan

  • pnpm --filter @discourse-graphs/obsidian check-types passes
  • getDgSchemaFileName("My Vault") returns dg-schema-my-vault.json
  • getDgSchemaFileName("") returns dg-schema-vault.json
  • ReactRootModal mounts and unmounts without memory leaks
  • useSchemaSelection initializes with all items selected
  • Toggling a triple auto-selects its required node/relation types

…export/import.

Adds DiscourseSchemaFile and DiscourseSchemaTemplate type definitions, plus
getDgSchemaFileName and DG_SCHEMA_EXPORT_VERSION — the minimal shared primitives
needed by both the schema export (ENG-1976) and import (ENG-1977) features.

Co-authored-by: Cursor <cursoragent@cursor.com>
@linear-code

linear-code Bot commented Jun 30, 2026

Copy link
Copy Markdown

ENG-1975

@supabase

supabase Bot commented Jun 30, 2026

Copy link
Copy Markdown

This pull request has been ignored for the connected project zytfjzqyijgagqxrzbmz because there are no changes detected in packages/database/supabase directory. You can change this behaviour in Project Integrations Settings ↗︎.


Preview Branches by Supabase.
Learn more about Supabase Branching ↗︎.

@vercel

vercel Bot commented Jun 30, 2026

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

1 Skipped Deployment
Project Deployment Actions Updated (UTC)
discourse-graph Skipped Skipped Jul 29, 2026 2:58am

Request Review

@devin-ai-integration devin-ai-integration Bot left a comment

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.

Devin Review found 2 potential issues.

Open in Devin Review

id: z.string(),
label: z.string(),
complement: z.string(),
color: z.string(),

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.

🟡 Schema validation accepts any color string but downstream types require one of 13 specific color names

The relation-type color is validated as any string (z.string() at apps/obsidian/src/utils/specValidation.ts:29), then force-cast to a type that only allows 13 specific color names, so an imported file with a bogus color like "rainbow" passes validation and silently violates the type contract.

Impact: Imported schema files with invalid color names pass validation, potentially causing unexpected rendering in the canvas UI.

Type mismatch between Zod schema and DiscourseRelationType.color

The DiscourseRelationType type at apps/obsidian/src/types.ts:27 defines color: TldrawColorName, which is a union of 13 specific string literals defined at apps/obsidian/src/utils/tldrawColors.ts:5-19 (e.g. "black", "blue", "red", etc.).

However, the Zod schema at line 29 uses z.string() which accepts any string. The parseDgSchemaFile function at line 82 then casts the result as DiscourseSchemaFile, hiding this mismatch from the type system.

Downstream code like COLOR_PALETTE[relationType.color] at apps/obsidian/src/components/canvas/utils/relationTypeUtils.ts:135 or direct prop assignment at apps/obsidian/src/components/canvas/overlays/DragHandleOverlay.tsx:371 relies on this being a valid TldrawColorName. While some call sites have fallbacks (e.g. toTldrawColor()), the validation layer should catch this at parse time rather than allowing invalid data through.

The fix would be to use z.enum(TLDRAW_COLOR_NAMES) instead of z.string() for the color field, matching the actual type constraint.

Suggested change
color: z.string(),
color: z.enum(TLDRAW_COLOR_NAMES),
Open in Devin Review

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Fixed: changed to z.enum(TLDRAW_COLOR_NAMES) — this now enforces the 13-value union at parse time, consistent with the downstream TldrawColorName type.

Comment on lines +54 to +63
export const dgSchemaFileSchema = z.object({
version: z.literal(DG_SCHEMA_EXPORT_VERSION),
exportedAt: z.string(),
pluginVersion: z.string(),
vaultName: z.string(),
nodeTypes: z.array(discourseNodeSchema),
relationTypes: z.array(discourseRelationTypeSchema),
discourseRelations: z.array(discourseRelationSchema),
templates: z.array(templateExportSchema),
});

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.

🚩 Zod default behavior silently strips extra properties from imported schema files

The Zod schemas use z.object({...}) which by default strips unknown properties. This means if a future version of the schema adds new fields (e.g., a description on DiscourseRelationType), importing a file from that newer version will silently drop those fields without any warning. This is a design choice worth being aware of — if round-trip fidelity matters (export → share → import), consider using .passthrough() on the top-level schema, or at least documenting that extra fields are intentionally dropped.

Open in Devin Review

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

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

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

Addressed: added .passthrough() to all sub-schemas (discourseNodeSchema, discourseRelationTypeSchema, discourseRelationSchema, templateExportSchema) and the top-level dgSchemaFileSchema. Unknown fields are now passed through rather than silently stripped, preserving forward compatibility when a newer schema version adds fields.

@trangdoan982 trangdoan982 changed the title ENG-1975 Add schema file contract and shared foundation for Obsidian export/import ENG-1975 Define schema file contract and shared foundation for Obsidian export/import Jul 29, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant