Carry a store's configuration in the registry record

A store's `config.json` lived in a repository the desktop client fetched over HTTP,
which made a store's behaviour depend on a second thing existing and staying
reachable. The registry already answers what a store *is*; it now answers how it
behaves too, in the same shape that file had, so this record is the one source of
truth and a store can be configured from the admin alone.

`storeRepositoryUrl` stays, demoted to a pointer for a person — where the store's
own repository is, when it has one. Clients released before this field still fetch a
`config.json` from it, so nothing has to move at once.

The column is nullable because a store that configures nothing is still a store: the
client falls back to the engine's built-in defaults, which need only a name and a
catalog. The admin edits it as JSON text through a pair of accessors, so the column
holds real JSON and invalid input comes back with the text kept and a message rather
than a 500.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-08-19 06:43:09 +02:00
co-authored by Claude Opus 5
parent 581d3c4371
commit 9255a11254
8 changed files with 179 additions and 25 deletions
@@ -19,11 +19,30 @@ RSpec.describe Api::StoresController, type: :request do
get "/api/stores"
store = JSON.parse(response.body).first
expect(store).to include("name", "catalogUrl", "storeRepositoryUrl")
expect(store).to include("name", "catalogUrl", "storeRepositoryUrl", "config")
expect(store["catalogUrl"]).to eq("https://teletypegames.org")
expect(store["storeRepositoryUrl"]).to end_with("/stores/ttg-desktop-store")
end
# The config is what the client applies instead of fetching a repository's
# config.json, so it has to come back as an object rather than as JSON text.
it "carries the store's own config as an object" do
create(:store, config: { "paths" => { "subfolder" => "teletypegames" } })
get "/api/stores"
config = JSON.parse(response.body).first["config"]
expect(config).to eq("paths" => { "subfolder" => "teletypegames" })
end
it "answers null for a store that configures nothing" do
create(:store, config: nil)
get "/api/stores"
expect(JSON.parse(response.body).first["config"]).to be_nil
end
it "answers null for a store with no repository" do
create(:store, store_repository_url: nil)
+46
View File
@@ -21,6 +21,52 @@ RSpec.describe Store, type: :model do
expect(build(:store, store_repository_url: "")).to be_valid
end
describe "config" do
it "accepts a store that configures nothing" do
expect(build(:store, config: nil)).to be_valid
end
it "rejects a config that is not a JSON object" do
store = build(:store, config: ["not", "an", "object"])
expect(store).not_to be_valid
expect(store.errors[:config]).to include("must be a JSON object")
end
# The admin form edits JSON text; the column holds real JSON. These two accessors
# are the only bridge between them, so both directions are worth pinning.
it "parses config_json into the column" do
store = build(:store, config_json: '{"paths": {"subfolder": "teletypegames"}}')
expect(store).to be_valid
expect(store.config).to eq("paths" => { "subfolder" => "teletypegames" })
end
it "renders the column back as pretty JSON text" do
store = build(:store, config: { "paths" => { "subfolder" => "teletypegames" } })
expect(JSON.parse(store.config_json)).to eq(store.config)
expect(store.config_json).to include("\n")
end
it "clears the config when the text is emptied" do
store = build(:store, config: { "paths" => {} })
store.config_json = ""
expect(store).to be_valid
expect(store.config).to be_nil
end
# Refused with the text kept, rather than raising: the form has to come back with
# what the person typed and a message they can act on.
it "refuses invalid JSON and keeps the text" do
store = build(:store, config_json: '{"paths": ')
expect(store).not_to be_valid
expect(store.errors[:config].first).to start_with("is not valid JSON")
expect(store.config_json).to eq('{"paths": ')
end
end
describe ".ordered" do
it "lists stores by name" do
later = create(:store, name: "Zed Games")