feat(store): let the store exchange configuration instead of rows - #3386
Merged
Conversation
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.
Contributor
Quality Report — ConductionNL/openregister @
|
| 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.
Contributor
Quality Report — ConductionNL/openregister @
|
| 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.
Contributor
Quality Report — ConductionNL/openregister @
|
| 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.
Contributor
Quality Report — ConductionNL/openregister @
|
| 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.
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.
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-sharingis the fleet standard. A schema opts itself in with one marker,x-openregister-shareable, andSchemaShareableConfigScannerturns 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, whichConfigSetShareableConfigTypedocuments 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-planeis what users see.CnStorePagecalls/api/store/items, the engine fetches from one remote instance, andGenericStoreInstallerwrites 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/referencedShareableConfigTypeRegistry,FederatedConfigServiceorIShareableConfigType, and theapphost-store-planespec never mentions federated config, shareable markers or config sets.What this does
A
storeblock may now declaretypes, the shareable configuration type ids it surfaces.FederatedConfigService::discover()across each declared type's topic.FederatedConfigService::install(), which routes to the owning type'sdeserialise(), 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. 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 foropenregister.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.installablekeeps 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
lib/filePsalm caught the DI factory in
Bootstrap.phpstill 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
typesto the manifest schema and shows the type and publisher on a card.