Skip to content

feat(store): let the store exchange configuration instead of rows - #3386

Merged
rubenvdlinde merged 5 commits into
developmentfrom
feat/store-over-federated-config
Sep 3, 2026
Merged

feat(store): let the store exchange configuration instead of rows#3386
rubenvdlinde merged 5 commits into
developmentfrom
feat/store-over-federated-config

Conversation

@rubenvdlinde

Copy link
Copy Markdown
Contributor

The problem

OpenRegister has two stores. They were built for the same purpose, neither references the other, and the weaker one is the one users can see.

federated-config-sharing is the fleet standard. A schema opts itself in with one marker, x-openregister-shareable, and SchemaShareableConfigScanner turns it into a shareable type with no per-app code. Three types ship built in: flows, registers with their schemas, and a whole configuration set, which ConfigSetShareableConfigType documents as "an app's worth of configuration at once: registers, schemas, objects, views, flows, sources and mappings". Bundles are signed, published, found by topic and gated on a per-org trusted-key list. Its spec calls this the store: "A schema SHALL be able to opt its objects into the store with a single marker."

Twenty one of its twenty five tasks are done. The backend works. It has no user interface, so nobody can reach it.

apphost-store-plane is what users see. CnStorePage calls /api/store/items, the engine fetches from one remote instance, and GenericStoreInstaller writes each component as a plain object into one allowlisted schema. It cannot carry a schema. It cannot carry a flow.

So the Store menu entry exchanges rows of one schema, and a municipality cannot publish the way it runs its council.

I verified the two are disconnected: nothing under lib/AppHost/ referenced ShareableConfigTypeRegistry, FederatedConfigService or IShareableConfigType, and the apphost-store-plane spec never mentions federated config, shareable markers or config sets.

What this does

A store block may now declare types, the shareable configuration type ids it surfaces.

  • Cards come from FederatedConfigService::discover() across each declared type's topic.
  • An install fetches the bundle and calls FederatedConfigService::install(), which routes to the owning type's deserialise(), so a configuration set arrives as registers, schemas, objects, views, flows, sources and mappings.
  • An untrusted publisher is refused before any fetch, whole rather than half.

An app that declares no types keeps the objects API it has today and makes no discovery call at all, so nothing that ships now changes. The two paths are selected by declaration, never by probing, and there is a test asserting the objects path never reaches discovery.

Two decisions worth vetoing

A conventional bundle path. Discovery returns repositories, and a repository is not a bundle. publish() takes a caller-supplied path, which is fine for a link a publisher hands you and useless for a browsable store: a card with no path can be browsed and never installed. The catalogue looks for openregister.json, then .openregister/config.json.

No schema allowlist on a configuration install. A set exists to introduce schemas the instance does not have yet, so a list of schemas the app already owns would refuse exactly the sets worth installing. The trust boundary for a bundle is its publisher, enforced by isSourceAllowed() and the trusted-key check. installable keeps its meaning on the objects path. The second commit rewrites the requirement I first wrote the other way round.

Where the first catalogue comes from

Nobody has to invent it. Each app already ships seed datasets, and they are configuration sets with a different file extension. Decidiq has four in lib/Settings/profiles/: a municipality with committees and factions, an association with a members' meeting, a company board, and a works council. That is the default gemeente, already written.

Checks

check result
phpunit (AppHost) 231 passed, 568 assertions
phpcs clean on every changed lib/ file
psalm no errors

Psalm caught the DI factory in Bootstrap.php still constructing the controller with the old argument list, which would have been a dispatch-time failure rather than a test failure.

Companion

Needs ConductionNL/nextcloud-vue#976, which adds types to the manifest schema and shows the type and publisher on a card.

OpenRegister has two stores and neither knows about the other.

federated-config-sharing is the fleet standard. A schema opts itself in
with x-openregister-shareable, the built-in types cover flows, registers
with their schemas, and a whole configuration set, and bundles travel
signed, discoverable and gated on a trusted-key list. Its own spec calls
this the store. It has no user interface, so nobody can reach it.

The apphost store plane is what users see. It fetches items from one
remote instance and writes each component as a plain object into one
allowlisted schema. It cannot carry a schema. It cannot carry a flow.

So the Store menu entry exchanges rows of one schema, and a municipality
cannot publish the way it runs its council.

A store block may now declare types, the shareable configuration type ids
it surfaces. Declaring them makes the store list what publishers have
published and install through the type that owns the bundle, so a
configuration set arrives as registers, schemas, objects, views, flows,
sources and mappings.

An app that declares no types keeps the objects API it has today and
makes no discovery call at all, so nothing that ships now changes.

Discovery returns repositories, and a repository is not a bundle, so the
catalogue also fixes a conventional bundle path. Without one a card can
be browsed and never installed.
…uration set

The requirement as first written said the installable allowlist and the
shareable marker both have to hold for a configuration install. That is
not what the code does, and on reflection it is not what it should do.

A configuration set exists to introduce registers, schemas and flows the
instance does not have yet. A list of schemas the app already owns
cannot express whether such a set may be applied: it would refuse
exactly the sets worth installing.

The trust boundary for a bundle is its publisher. The allowlist keeps
its meaning on the objects path, where an item names a schema the app
does own.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 23f6cf3

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-03 19:44 UTC

Download the full PDF report from the workflow artifacts.

phpcs refuses an inline if and phpmd refuses an else, so the branch that
picks a catalogue moves into two private methods that return early.

Two suppressions with their reasons. The controller holds both store
paths because one route alias serves every app, and splitting it would
put that alias in two places. StoreManifest mirrors the manifest block
one key to one parameter, and a grouping layer is exactly where a
silently dropped key hides.

The coverage ratchet caught resolve() and the bundle-path fallback
having no test at all, which is fair: they are the half that decides
what gets installed. Nine tests added, covering a slug resolving to its
bundle, every conventional path being tried, a contained discovery
failure, and both shapes a type may report its components in.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 15927b8

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-03 19:57 UTC

Download the full PDF report from the workflow artifacts.

The coverage ratchet was right that the branch choosing a catalogue had
no test on the federated side at all. Five tests: search routes to the
catalogue and never to the objects client, the declared type ids become
the kind filters when the app names none, install routes through the
catalogue rather than the object installer, an unresolved slug is a 404
rather than a blank install, and a catalogue failure reports unreachable
instead of raising.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 87e6230

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-03 20:18 UTC

Download the full PDF report from the workflow artifacts.

Seven more, aimed at what the ratchet says is still uncovered rather
than at what reads well: resolve walking past a type nothing owns to one
that does, a contained discovery failure, an entry naming no repository,
the second conventional bundle path answering when the first does not,
an empty bundle treated as no answer, and both shapes a type can report
components in that are neither a name nor a descriptor.

No coverage driver is installed locally, so these were chosen by reading
the branches rather than measured. CI is the measurement.
@github-actions

github-actions Bot commented Sep 3, 2026

Copy link
Copy Markdown
Contributor

Quality Report — ConductionNL/openregister @ 2e0da3f

Check PHP Vue Security License Tests
lint
phpcs
phpmd
psalm
phpstan
phpmetrics
eslint
stylelint
build
check-specs
test-l10n
test-l10n-parity
format
check-schema-l10n
check-l10n-js
composer ✅ 174/174
npm ✅ 543/543
app:check-code ⏭️
info.xml
REUSE
PHPUnit
Newman
Playwright ⏭️ deferred — runs on the promotion into beta/main, not on a pull request into development
Hydra gates

Quality workflow — 2026-09-03 20:33 UTC

Download the full PDF report from the workflow artifacts.

@rubenvdlinde
rubenvdlinde merged commit eb3f616 into development Sep 3, 2026
46 checks passed
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