Skip to content

Fully parse the exported connection file with a Zod wire-format schema#1822

Merged
kmcginnes merged 10 commits into
mainfrom
explicit-exported-connection-type
Jun 18, 2026
Merged

Fully parse the exported connection file with a Zod wire-format schema#1822
kmcginnes merged 10 commits into
mainfrom
explicit-exported-connection-type

Conversation

@kmcginnes

@kmcginnes kmcginnes commented Jun 14, 2026

Copy link
Copy Markdown
Collaborator

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 via Pick<ConfigurationContextProps, ...>. That borrowing keeps the unused in-memory RawConfiguration.schema field 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.ts is 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 uncaught TypeError on a malformed schema.vertices/edges instead of rejecting with the "Invalid File" toast.

Key properties of the schema:

  • Lenient. Every level is a looseObject, so unknown and legacy fields — per-type styling, __inferred/__matches on prefixes, attribute dataType, optional connection fields — pass through untouched into the parsed output.
  • Validating. Every value the type promises is actually checked: id, connection URL and query engine, and the structural shape of vertices/edges/prefixes/edge-connections. Both the connection url and the optional graphDbUrl are constrained to http(s) via Zod's built-in z.url({ protocol: /^https?$/ })graphDbUrl is forwarded verbatim as the proxy's request target, so an imported file must not be able to point it at another scheme.
  • Produces ready-to-store values. Branded ids and types are applied in the schema via .transform(), and lastUpdate is coerced from its ISO string into a real Date via z.coerce.date(). The importer consumes the parsed output directly and drops its manual new Date(...) revival and field-by-field reconstruction.

The guard becomes a parser: parseConnectionFile(data): ExportedConnectionFile | null. The same ExportedConnectionFile type describes both ends of the round trip: the writer assigns a Date for lastUpdate and JSON.stringify serializes it to the ISO string the parser later coerces back — so one type covers the format without a separate input view.

Vertex and edge attributes are consistently optional and default to [].

Suggested reading order:

  1. parseConnectionFile.ts — the Zod schema (source of truth), the inferred ExportedConnectionFile type, and parseConnectionFile (core change).
  2. saveConfigurationToFile.ts — now typed against ExportedConnectionFile.
  3. useImportConnectionFile.ts — consumes the parsed output directly; manual Date revival and per-field reads removed.
  4. Tests + CONTEXT.md glossary entry.

Two small, accepted behavior changes:

  • A connection-less export emits connection.url: "" instead of omitting url (forced by the type; aligns the writer with the parser, which requires url). This is a not-actually-reachable state and is pinned by a test noting it should disappear in a later slice.
  • The connection URL is now whitespace-trimmed on parse (a property of 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 checks passes (lint, format, types).
  • pnpm test passes — full suite.
  • Tests pin: the malformed-schema rejection (previously threw); branded/Date-coerced parsed output; unknown/legacy field passthrough; the attribute defaulting; the http(s) refinement of url and graphDbUrl; URL whitespace trimming; the on-disk lastUpdate ISO bytes (so the writer's lastUpdate type can change without altering the file); the save→parse round-trip (values, not just parseability); and the url: "" asymmetry. Backward-compat pins for legacy imports (__matches) and the embedded-schema envelope stay green.

Related Issues

Check List

  • I confirm that my contribution is made under the terms of the Apache 2.0 license.
  • I have verified pnpm checks passes with no errors.
  • I have verified pnpm test passes with no failures.
  • I have covered new added functionality with unit tests if necessary.
  • I have updated documentation if necessary.

@kmcginnes kmcginnes changed the title Give exported connection files an explicit wire-format type Fully parse the exported connection file with a Zod wire-format schema Jun 17, 2026
Base automatically changed from simplify-configuration-merge to main June 17, 2026 22:58
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.
@kmcginnes kmcginnes force-pushed the explicit-exported-connection-type branch from 12504f8 to b763385 Compare June 17, 2026 23:02
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.
@kmcginnes kmcginnes marked this pull request as ready for review June 18, 2026 15:59
@kmcginnes kmcginnes merged commit 37e48cc into main Jun 18, 2026
6 checks passed
@kmcginnes kmcginnes deleted the explicit-exported-connection-type branch June 18, 2026 22:01
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.

Give exported connection files an explicit wire-format type

2 participants