Read a store's config from the registry record
`GET /api/stores` records now carry a `config` field — a store's `config.json` moved into the record that already said what the store is — and the client applies it directly. Installing a store no longer depends on a second repository existing and staying reachable, and a store can be configured from the site's admin alone. The order is registry config, then a repository's `config.json`, then the engine's defaults. The middle one is why nothing has to move at once: a registry whose stores have not been migrated is read exactly as before. The window cannot supply a config. It is handed stores to show and hands one back to install, but only as an identity: `RegistryStoreDtoMapper.toModel` drops the config and `StoreProvisioningService` reads the record again from the registry first. A config decides where files are written and, through `paths.subfolder`, which subtree the store may later delete from — not a decision the renderer gets to make, for the same reason a `GameDto` carries no paths. Tested by installing from a record carrying `subfolder: "ATTACKER"` and `install_root: "/tmp/pwned"` and finding neither on disk. A store that has left the registry, or a registry that cannot be re-read, still installs: it falls back to the engine's defaults rather than refusing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,7 +1,7 @@
|
||||
import { RegistryUnavailableError } from '../../domain/errors/RegistryUnavailableError'
|
||||
import type { RegistryStore } from '../../domain/models/RegistryStore'
|
||||
import type { StoreRegistryRepository } from '../../domain/ports/StoreRegistryRepository'
|
||||
import { asRecord, readString, type JsonRecord } from '../json/JsonRecord'
|
||||
import { asRecord, readRecord, readString, type JsonRecord } from '../json/JsonRecord'
|
||||
import { BuildConfiguration } from '../config/BuildConfiguration'
|
||||
import type { HttpTextClient } from '../http/HttpTextClient'
|
||||
|
||||
@@ -15,10 +15,10 @@ const DEFAULT_REGISTRY_URL = 'https://teletypegames.org/api/stores'
|
||||
* field a build was packaged with (for shipping a client for another site), and finally
|
||||
* the address of ours.
|
||||
*
|
||||
* A record needs a name and a catalog URL; those two make a store. The repository
|
||||
* is optional and arrives as null when absent — a store configured by nothing but
|
||||
* this record installs on the engine's defaults. Records missing either of the two
|
||||
* required fields are dropped rather than half-used.
|
||||
* A record needs a name and a catalog URL; those two make a store. The config and the
|
||||
* repository are both optional and arrive as null when absent — a store configured by
|
||||
* nothing but this record installs on the engine's defaults. Records missing either of
|
||||
* the two required fields are dropped rather than half-used.
|
||||
*/
|
||||
export class HttpStoreRegistryRepository implements StoreRegistryRepository {
|
||||
public readonly sourceUrl: string
|
||||
@@ -53,7 +53,8 @@ export class HttpStoreRegistryRepository implements StoreRegistryRepository {
|
||||
return {
|
||||
name: readString(row, 'name').trim(),
|
||||
catalogUrl: (readString(row, 'catalogUrl') || readString(row, 'catalog_url')).trim(),
|
||||
storeRepositoryUrl: repository.length > 0 ? repository : null
|
||||
storeRepositoryUrl: repository.length > 0 ? repository : null,
|
||||
config: readRecord(row, 'config')
|
||||
}
|
||||
})
|
||||
.filter((store: RegistryStore): boolean =>
|
||||
|
||||
@@ -77,13 +77,15 @@ export class NativeStoreEngineInstaller implements StoreEngineInstaller {
|
||||
/**
|
||||
* The store's configuration.
|
||||
*
|
||||
* Three cases, and all of them install:
|
||||
* Four cases, and all of them install:
|
||||
*
|
||||
* - **a repository with a config.json** — that file is the authority on how the
|
||||
* store behaves: which platforms it offers, which statuses it shows, where
|
||||
* things land;
|
||||
* - **a config on the registry record** — the authority on how the store behaves:
|
||||
* which platforms it offers, which statuses it shows, where things land. No
|
||||
* request at all, because it arrived with the store list;
|
||||
* - **a repository with a config.json** — the same thing in its older home, read
|
||||
* for a registry whose stores have not moved over yet;
|
||||
* - **a repository without one** (404) — the engine's defaults, as below;
|
||||
* - **no repository at all** — the same defaults, without the round trip.
|
||||
* - **neither** — the same defaults, without the round trip.
|
||||
*
|
||||
* The engine's built-in defaults already cover the host-to-asset mapping, the
|
||||
* modes, the platforms and the behaviour, so what a store actually has to supply is
|
||||
@@ -113,6 +115,13 @@ export class NativeStoreEngineInstaller implements StoreEngineInstaller {
|
||||
storeId: string,
|
||||
progress: EngineProgressListener
|
||||
): Promise<Record<string, unknown>> {
|
||||
// The registry's own answer wins, and needs no request: a store's configuration is
|
||||
// part of its record now rather than a file in a repository that has to exist and
|
||||
// stay reachable.
|
||||
if (store.config !== null) {
|
||||
progress.onLog?.(`${store.name} is configured by the registry`)
|
||||
return { ...store.config }
|
||||
}
|
||||
const repositoryUrl = store.storeRepositoryUrl
|
||||
if (repositoryUrl === null) {
|
||||
progress.onLog?.(`${store.name} has no store repository — using the engine defaults`)
|
||||
|
||||
Reference in New Issue
Block a user