Skip to content

refactor(solid): put the Solid client libraries behind the app's own modules - #324

Merged
ixuz merged 8 commits into
mainfrom
solid-client-seams
Oct 26, 2026
Merged

ixuz merged 8 commits into
mainfrom
solid-client-seams

Conversation

@ixuz

@ixuz ixuz commented Oct 26, 2026 •

Copy link
Copy Markdown
Member

What and why

Replacing the Solid client libraries used to mean touching most of packages/solid and its tests at once. The libraries were imported in more than 40 files, internals were read in several places, and tests mocked the libraries directly. This pull request moves every use of them behind modules of the app's own. A later switch is then new code behind fixed interfaces, with no changes in the callers. Nothing changes for users, and nothing changes in what the app writes to a pod.

  • One folder for the libraries. Only packages/solid/src/client/ imports them: rdf.ts (the data model), resources.ts (reading and writing pods, with an own HttpError), session.ts (the sign-in client), access.ts (the public-read grant) and linearDataset.ts. packages/shacl/src/turtleDocument.ts is shacl's only importer. The types get the app's own names (Dataset, Subject, NamedSubject, SubjectBuilder). The functions keep the names the code already called, so callers changed their imports and type names only.
  • No library internals outside client/. The change log, the resource info the ETag map is keyed by, a subject's predicates and a dataset's graphs are reached through functions rdf.ts exports (changesOf, sourceOf, propertiesOf, datasetFromQuads, withQuads, isEmptyDataset, parseQuads).
  • The implicit behavior is written down. client/rdf.ts documents it and client/rdf.test.ts pins it against the current library:
    • listing subjects skips blank nodes;
    • local names resolve against the source URL;
    • the date-reading rule (GHSA-7p4m);
    • language tags are lowercased;
    • decimals, as read and as written.
  • Errors. HttpError keeps the library's statusCode, response.status and message, which retryable.ts and typeIndex.ts rely on, and carries the library's error as cause. Its name is HttpError, so the crash screen's copyable details now start with HttpError: instead of Error:.
  • Sign-in in two parts. solidSessionGateway.ts keeps the logic that depends on no library: the login hash, issuer discovery, and 401 as expiry. It takes the client from client/session.ts, which provides restore, login, logout, and a fetch and a WebID that are always the current session's. composition gets one factory, createSolidSession, that returns { sessionGateway, fetch, webId }. The own-pod release reads from fix(composition): read a release outside the user's own pod as no one #325 ask that same client who is logged in.
  • Grant and profiles injected. createSolidReleasePublisher takes grantPublicRead, and the storage gateway takes its profile lookup (getProfileStorages). The publisher's fence and its server workarounds stay around the grant. The publisher's tests also run the real grant through the fence, for WAC and ACP.
  • Tests go through the app's seams. Fixtures are built in src/testing/, and tests mock the app's modules, not the libraries'.
  • The boundary rule. npm run check:boundaries lets only client/ and shacl's module import an @inrupt/ package outside tests. Only client/access.ts may import the access-control functions.
  • Docs. vendor-code.md now states what a swap really costs. A replacement provides the client/ modules with the current library's type shapes, because callers read subject.url and use the builder chain, and it matches the behavior their tests pin. boundaries.md, architecture.md, testing.md, authentication.md, the package's README and a few links follow.

Out of scope, as the issue says: a second implementation, a build-time switch and choosing a replacement library.

Closes: #323

Checklist

  • One issue. This pull request addresses one issue, linked above.
  • One step. This pull request does not finish its issue, so it says Refs:, not Closes:. Leave this unchecked otherwise.
  • Done, as AGENTS.md defines it: npm run check passes (lint, Markdown and spelling included), npm run lint:reuse too if a file was added, and npm run crosscheck too if ns/ or decks/ changed. (crosscheck n/a: ns/ and decks/ are unchanged)
  • Tests. New and changed code has its tests, and every package stays at 100% coverage (docs/testing.md).
  • Docs. The docs say what changed, and a new page in docs/ is listed in docs/docs.ttl. (no new page)
  • Languages. A new or changed message is in every locale: en, sv, ko, de, es and fr (packages/ui/src/i18n/). (n/a: no message changed)
  • Vocabulary and shapes. Any change to ns/ was agreed in an issue first; no published ns/shapes/*/vN.ttl or decks/*/vN.ttl was edited; generated files were regenerated, not edited. (n/a: ns/ and decks/ are unchanged)
  • Breaking? Nothing in the public API breaks, or a commit says so (! and a BREAKING CHANGE: footer). npm run check:breaking sees a line of api/ that goes, but not a feature removed, nor a reader that reads less than before.
  • Pod data. Any change to what the app writes to a pod comes with its migration (docs/migrations.md). (n/a: the app writes the same data and requests)
  • Policy. Any change to an external rule the code follows (SM-2, the plural rules, a specification) is cited. (n/a: no external rule changed)
  • Licenses. New content is MIT, or a deck states its own license; a new file has its SPDX header (npm run lint:reuse), or a new deck its line in REUSE.toml; third-party content is credited.
  • Architecture. Imports follow docs/boundaries.md and the layers of docs/architecture.md.
  • Run. I ran the app (npm run dev) and tried the change, and considered the user journeys.
  • Authorship. I read and understand every line of this pull request, and answer for it, whatever tools helped write it (CONTRIBUTING.md).
  • Commits. Each commit is one logical change, in Conventional Commits form (type(scope): subject, ! and a BREAKING CHANGE: footer if it breaks something), with an imperative subject and a body that explains why (CONTRIBUTING.md).

@ixuz ixuz added the enhancement New feature or request label Oct 26, 2026
ixuz added 8 commits October 26, 2026 21:14
The package's files named @inrupt/solid-client's data model everywhere:
its dataset and subject types and its readers and builders, in every
mapper, in records.ts, ownership.ts and datasets.ts. Swapping or only
trying out another library would have meant touching all of them.

client/rdf.ts now gives the data model names of the app's own (Dataset,
Subject, NamedSubject, SubjectBuilder, and datasets that know their
source) and passes the operations through under the names the code
already calls, so the callers change only their imports and type names.
It is a thin layer over the same library, so nothing behaves or writes
differently. Reading and writing pods, sign-in and access control still
call the library and get their own modules next.

Refs: #323
…odule

The change log, the resource info, a subject's predicates and a
dataset's graphs were read and built in several files across the
package, so a switch of libraries would have meant changes well beyond
the RDF module. They now sit behind functions client/rdf.ts exports
(changesOf, sourceOf, datasetFromQuads, withQuads, isEmptyDataset,
propertiesOf, localNameOf, parseQuads), and the one-pass dataset
builder moves into client/ beside it, since it rebuilds the library's
structure.

The module's doc comment and its tests now also state what the app
relies on of the library beyond the names of its calls: blank-node
subjects left out of getThingAll, subjects made by name resolved
against the document read, how dates, language tags and decimals are
read. A second implementation has to match each of these.

Refs: #323
Every read and write of a pod's resources now goes through
client/resources.ts, so that the rest of the package names no Solid
client library and a later switch of libraries is new code behind the
same calls. The module keeps the library's names and requests (headers,
URLs and the PATCH a saved edit sends, with datasets.ts's own body and
the library's where it writes none), and the Turtle and JSON-LD parsers
move there from the RDF module.

A failure the server answers with is now an HttpError of the app's own,
with the library's message, status and response, so the checks of
statusCode and response.status in datasets.ts, containers.ts, the copier,
the release drafts and the UI's retryable keep working, as does the
message the type index shows.

The SHACL package, which may not import the solid package, reads Turtle
through one module of its own, turtleDocument.ts, the only place it
names the library.

Refs: #323
The session gateway and authFetch called the authentication library
directly, so a switch to another Solid-OIDC client would have meant
rewriting the gateway's own logic with it. The library now sits behind
a small sign-in client in client/session.ts: restore, login, logout,
and a fetch and a WebID that are always the current session's. The
gateway keeps what depends on no library, the route a login returns
to, issuer discovery and the client ID choice, and takes the client it
is given.

The composition gets the gateway, the authenticated fetch and the
logged-in WebID from one factory, createSolidSession, so all three act
on the same client. The gateway's tests now run on a fake client, and
the client's own tests cover the library calls.

Refs: #323
The release publisher and the storage gateway called the Solid client
library themselves, so a switch of library would have meant rewriting
both and their tests. The public-read grant now lives in
client/access.ts, with the request fixes it needs for node-solid-server
and the Community Solid Server, since those work around how this
library writes access control. The fence that keeps the grant to the
release's own ACL or ACR stays in the publisher, around whatever grant
it is given, because that is the app's rule and not the library's.

The profile lookup becomes getProfileStorages in client/resources.ts,
and the gateway takes it as an option. Composition wires both, the
publisher's and gateway's tests run on fakes of the injected functions,
and the boundary rule for access control now points at client/access.ts.

Refs: #323
The tests still built their datasets with the library's mock helpers
and mocked the library's modules, so a switch of libraries would have
meant rewriting most of the package's tests as well as client/. They
now build datasets with testing/datasets.ts, which uses the RDF module's
new datasetAt, and they mock client/resources and client/rdf instead of
the library. The resources tests now mock getSolidDatasetLinear,
because that is what the code calls. Only the tests of client/ still
import the library.

The patch tests now make their changes with edits through the RDF
module instead of writing the library's change log by hand. A change
with a blank node, which those edits never record, comes from a mocked
changesOf. The review state tests edit subjects with buildThing instead
of rewriting their predicates.

With no test mocking the library's getSolidDataset any more, the check
in linearDataset.ts for a document the library parsed without our
parser could never be true: the library is given only our parser, and
it throws on any other type. The check is removed.

Refs: #323
The earlier steps moved every use of @inrupt/solid-client and
@inrupt/solid-client-authn-browser behind modules of the app's own in
packages/solid/src/client/, with one Turtle reader in shacl. Nothing kept
it that way: a new file could import the libraries again and quietly
widen what a later switch would have to touch. The boundary check now
fails on any import of either library, of any form, outside those files,
tests aside, and still keeps the access control to the public-read
grant. docs/boundaries.md names the client modules and the rule.

Refs: #323
The client libraries now sit behind modules of the app's own in
packages/solid/src/client/ and shacl's turtleDocument.ts, but the docs
still said the libraries were used throughout the package, that the
mappers translate Inrupt's types and that tests mock @inrupt modules.
The cost of a swap is now known: a second implementation of those
modules with no caller changes. The vendor page names that cost and the
behavior a replacement has to match, and the architecture, testing,
authentication, privacy, standards and data model pages, the package's
README and its description follow.

Closes: #323
@ixuz
ixuz force-pushed the solid-client-seams branch from 648195e to 861b329 Compare October 26, 2026 20:36
@ixuz
ixuz enabled auto-merge October 26, 2026 20:40
@ixuz
ixuz added this pull request to the merge queue Oct 26, 2026
Merged via the queue into main with commit 5fdce58 Oct 26, 2026
35 checks passed
@ixuz
ixuz deleted the solid-client-seams branch October 26, 2026 21:00
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

enhancement New feature or request

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Replacing the Solid client libraries means rewriting most of packages/solid, not only its ports

1 participant