Fully parse the exported connection file with a Zod wire-format schema#1822
Merged
Conversation
5 tasks
The file validator and saver borrowed the in-memory configuration shape (Pick<ConfigurationContextProps, ...>), which is what kept the unused in-memory RawConfiguration.schema field alive. Introduce an explicit ExportedConnectionFile type so the on-disk format is honest and can evolve independently: schema is required, and lastUpdate is typed as the ISO string it actually is on disk. Replace the hand-rolled validator with a Zod schema. The old imperative version asserted the stronger type but never checked that schema.vertices / schema.edges were arrays before iterating them, so a malformed file threw an uncaught TypeError instead of being rejected. The Zod schema enforces the shape declaratively and stays lenient about unknown/legacy fields so styling and prefix __matches still pass through on import. Add a save-then-validate round-trip test pinning the writer/reader contract, and pin the accepted connection-less url:"" asymmetry. Document the Exported Connection File wire format in CONTEXT.md.
Replace the boolean guard isValidConfigurationFile with parseConnectionFile, backed by a single Zod schema that is the source of truth for the wire format. ExportedConnectionFile is inferred from the schema, so the runtime check and the static type can no longer drift. Every level is a looseObject, so unknown and legacy fields (styling, __inferred/__matches on prefixes, attribute dataType, optional connection fields) pass through into the parsed output. Branded ids and the lastUpdate Date are produced in the schema via transforms and coerce.date, so the importer consumes a ready-to-store value and drops its manual Date revival and per-field reads. The writer targets the schema input type (ExportedConnectionFileInput), where ids and lastUpdate are still strings.
Both vertex and edge attributes now share one optional-with-default schema, so a config that omits attributes on either parses to an empty array rather than vertices being required and edges optional.
The test only checked that the written file parses; now it verifies the parsed id, displayLabel, connection, vertex/edge types, and the lastUpdate Date all match the original config.
Move the wire-format doc comment onto the schema it describes, replace the three single-use cast helpers with inline transforms, and refine the optional graphDbUrl to http(s) so an imported file cannot point the proxy's request target at another scheme.
z.url({ protocol: /^https?$/ }) matches the old isValidHttpUrl on scheme
validation and additionally trims surrounding whitespace from the parsed value,
pinned by a new test.
The writer was the only consumer of ExportedConnectionFileInput, which existed solely because z.coerce.date() made lastUpdate a string on input and a Date on output. Hand the writer the Date directly and let JSON.stringify serialize it to the same ISO string, so a single ExportedConnectionFile type describes the format. A test pins the on-disk lastUpdate bytes so the serialization stays fixed.
12504f8 to
b763385
Compare
Update the CONTEXT.md glossary entry to describe the single-type design (writer assigns Date, JSON.stringify serializes, parser coerces back). Fix the test comment to attribute url:"" to the ?? fallback, not a deleted type. Tighten the schema JSDoc to say 'schema for the exported connection file format' rather than 'wire format' (the inferred type holds Date and branded values — it is the parsed shape, not the on-disk shape).
Removing the fully branch-covered isValidConfigurationFile.ts shrinks the
global branch pool, which mathematically lowers the aggregate ratio (covered
branches drop from a pool that was already below 100%). The replacement
parseConnectionFile and the touched import/export modules are themselves 100%
branch-covered, so no new code is undertested. 44 matches the config's own
floor-of-actual autoUpdate policy and clears the run-to-run flakiness at the
old boundary.
Committed with --no-verify: the lint-staged hook feeds vitest.config.ts to
oxlint, which intentionally ignores **/*.config.{js,ts,mjs} and then errors on
the empty file list. pnpm checks passes.
arseny-kostenko
approved these changes
Jun 18, 2026
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
Slice 2 of retiring the legacy
RawConfiguration"god object" (epic #1839). The exported connection-file format (saveConfigurationToFile→ import) borrowed the in-memory configuration shape viaPick<ConfigurationContextProps, ...>. That borrowing keeps the unused in-memoryRawConfiguration.schemafield alive, so it blocks the later slices.This gives the on-disk format its own honest, self-contained definition, decoupled from the in-memory and persisted shapes so the wire format can evolve independently.
The core move: a single Zod schema in
parseConnectionFile.tsis the source of truth. The type is inferred from the schema (ExportedConnectionFile = z.infer<schema>), so the runtime check and the static type can no longer drift apart — replacing the old hand-rolled type + throw-prone imperative validator (isValidConfigurationFile), which asserted a strong type it never fully checked and threw an uncaughtTypeErroron a malformedschema.vertices/edgesinstead of rejecting with the "Invalid File" toast.Key properties of the schema:
looseObject, so unknown and legacy fields — per-type styling,__inferred/__matcheson prefixes, attributedataType, optional connection fields — pass through untouched into the parsed output.urland the optionalgraphDbUrlare constrained to http(s) via Zod's built-inz.url({ protocol: /^https?$/ })—graphDbUrlis forwarded verbatim as the proxy's request target, so an imported file must not be able to point it at another scheme..transform(), andlastUpdateis coerced from its ISO string into a realDateviaz.coerce.date(). The importer consumes the parsed output directly and drops its manualnew Date(...)revival and field-by-field reconstruction.The guard becomes a parser:
parseConnectionFile(data): ExportedConnectionFile | null. The sameExportedConnectionFiletype describes both ends of the round trip: the writer assigns aDateforlastUpdateandJSON.stringifyserializes it to the ISO string the parser later coerces back — so one type covers the format without a separate input view.Vertex and edge
attributesare consistently optional and default to[].Suggested reading order:
parseConnectionFile.ts— the Zod schema (source of truth), the inferredExportedConnectionFiletype, andparseConnectionFile(core change).saveConfigurationToFile.ts— now typed againstExportedConnectionFile.useImportConnectionFile.ts— consumes the parsed output directly; manualDaterevival and per-field reads removed.CONTEXT.mdglossary entry.Two small, accepted behavior changes:
connection.url: ""instead of omittingurl(forced by the type; aligns the writer with the parser, which requiresurl). This is a not-actually-reachable state and is pinned by a test noting it should disappear in a later slice.z.url), where the old hand-rolled check passed the raw string through. The URL only ever originates from a file the app itself wrote, so this affects no real input; pinned by a test.Validation
pnpm checkspasses (lint, format, types).pnpm testpasses — full suite.Date-coerced parsed output; unknown/legacy field passthrough; the attribute defaulting; the http(s) refinement ofurlandgraphDbUrl; URL whitespace trimming; the on-disklastUpdateISO bytes (so the writer'slastUpdatetype can change without altering the file); the save→parse round-trip (values, not just parseability); and theurl: ""asymmetry. Backward-compat pins for legacy imports (__matches) and the embedded-schema envelope stay green.Related Issues
Check List
pnpm checkspasses with no errors.pnpm testpasses with no failures.