ENG-1975 Define schema file contract and shared foundation for Obsidian export/import - #1180
ENG-1975 Define schema file contract and shared foundation for Obsidian export/import#1180trangdoan982 wants to merge 4 commits into
Conversation
…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>
|
This pull request has been ignored for the connected project Preview Branches by Supabase. |
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
| id: z.string(), | ||
| label: z.string(), | ||
| complement: z.string(), | ||
| color: z.string(), |
There was a problem hiding this comment.
🟡 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.
| color: z.string(), | |
| color: z.enum(TLDRAW_COLOR_NAMES), |
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
Fixed: changed to z.enum(TLDRAW_COLOR_NAMES) — this now enforces the 13-value union at parse time, consistent with the downstream TldrawColorName type.
| 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), | ||
| }); |
There was a problem hiding this comment.
🚩 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.
Was this helpful? React with 👍 or 👎 to provide feedback.
There was a problem hiding this comment.
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.
…h export and import)
Summary
Ships the shared foundation that the schema selection panel, export, and import PRs all stack on top of.
types.ts— addsDiscourseSchemaFileandDiscourseSchemaTemplatetype definitions for the exported JSON file formatspecValidation.ts— addsgetDgSchemaFileName(vaultName?)(kebab-cases vault name →dg-schema-<vault>.json) andDG_SCHEMA_EXPORT_VERSION = 1ReactRootModal.tsx— abstract ObsidianModalsubclass that mounts a React root intocontentEland unmounts on close; DRYs thecreateRoot/unmountboilerplate that was previously copy-pasted across 4 existing modalsuseSchemaSelection.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:
Test plan
pnpm --filter @discourse-graphs/obsidian check-typespassesgetDgSchemaFileName("My Vault")returnsdg-schema-my-vault.jsongetDgSchemaFileName("")returnsdg-schema-vault.jsonReactRootModalmounts and unmounts without memory leaksuseSchemaSelectioninitializes with all items selected