Skip to content

feat(r3-b): put every screen behind the repository contract - #63

Open
Morialkar wants to merge 2 commits into
mainfrom
feat/r3-b-repository-layer
Open

feat(r3-b): put every screen behind the repository contract#63
Morialkar wants to merge 2 commits into
mainfrom
feat/r3-b-repository-layer

Conversation

@Morialkar

Copy link
Copy Markdown
Owner

First step of R3-B. The epic calls for a local SQLite driver, and there was nowhere to plug one in: the client called apiClient directly from 27 files, 96 call sites, with no abstraction between screens and transport. D8's working agreement puts storage behind interfaces in packages/core precisely so the shell choice stays reversible; this pays that off.

What changed

packages/core now owns the domain. Table and Database had each been redeclared seven times across routes and components, with fields that had drifted apart. RecordEntity, ViewConfig, Share, Template, FieldType and the record value types now have one home, and lib/apiTypes re-exports from core instead of redeclaring.

The contract is eight repositories — databases, tables, fields, records, views, reports, shares, templates — as plain promises. Query keys, caching and optimistic updates stay in the client; the interfaces are free of TanStack Query concepts so a driver never has to know about them.

httpRepository is the hosted driver, injected through RepositoryProvider. That provider is the swap point: the local-first shells will pass a SQLite-backed driver and no screen will know.

Field normalisation moved into the driver. The API may omit options and validation; the domain type promises them, so the boundary that turns API shape into domain shape fills them in — rather than each of the callers that used to do it inconsistently.

A real bug this surfaced

RecordDetailView typed the views endpoint as an envelope and read viewsQuery.data?.data. The endpoint returns a bare arrayViewFeatureTest asserts a root count of two. Reading .data off an array gives undefined, so the list was always empty and no card view ever applied in the record detail view.

Two unit tests mocked the same envelope, which is exactly why they passed while the feature was broken. Their mocks now match what the API returns.

Contract guesses that were wrong

Writing the interfaces against the real call sites corrected several assumptions I had made from the endpoint names:

Assumed Actual
direction sort_dir
version_id for restore log_id
to_record to_record_id
filters as a map filters as an array
install by template_id the whole payload travels
bare arrays from history / trash / referencing-records / map-points all wrapped in data

CSV import was missing from the contract entirely — it goes through multipart.

Verification

  • zero apiClient calls remain outside lib/ and tests
  • 82/82 client tests, tsc -b, ESLint (0 errors), Prettier clean

Not in this PR

The SQLite mirror schema, the local driver, the offline queue and the no-auth local mode — the rest of R3-B. They now have a seam to attach to.

…creens

R3-B needs a seam for a local SQLite driver, and there was none: the client
called apiClient from 27 files, 96 call sites, with no abstraction in between.
D8's working agreement puts storage behind interfaces in packages/core so the
shell choice stays reversible; this starts paying that off.

packages/core now owns the canonical entity shapes. Table and Database had
each been redeclared seven times across routes and components, with fields
that had drifted apart. FieldType moves here too, so drivers and UI cannot
diverge on the registry's closed set.

The contract is deliberately plain promises: query keys, caching and
optimistic updates stay in the client.

Writing it against the real call sites corrected several guesses: the list
endpoint reads sort_dir rather than direction, restore-version is keyed by
log_id rather than a version, reassign-links expects to_record_id, CSV import
goes through multipart and was missing entirely, and history, trash and
referencing-records all answer inside a data envelope.

Nine files migrated so far; the remaining eighteen follow.
All 96 call sites across 27 files now go through the contract instead of
apiClient, so a SQLite driver can replace the HTTP one without touching a
screen. No apiClient call remains outside lib/ and tests.

Local entity declarations are gone in favour of packages/core: Table and
Database had been written out seven times each, and RecordEntity, ViewConfig,
Share, Template and the record value types now have one home. lib/apiTypes
re-exports from core rather than redeclaring.

Field option normalisation moved into the driver. The API may omit options
and validation; the domain type promises them, so the boundary that turns API
shape into domain shape is the right place to fill them in, rather than every
caller.

Two defects surfaced while writing the contract against real call sites.

RecordDetailView typed the views endpoint as an envelope and read
`viewsQuery.data?.data`, but the endpoint returns a bare array — ViewFeatureTest
asserts a root count. Reading `.data` off an array yields undefined, so the
list was always empty and no card view ever applied in the detail view. Two
unit tests mocked the same envelope, which is why they passed while the
feature was broken; their mocks now match the API.

Several contract guesses were also wrong and were corrected against the code:
restore-version is keyed by log_id, reassign-links expects to_record_id,
filters travel as an array rather than a map, template install sends the whole
payload rather than an id, and history, trash, referencing-records and
map-points all answer inside a data envelope.
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