7.7 KiB
Architecture Decision Records
ADR-1: The source is TypeScript, linked into an application while both change
Status: Accepted
Context: manage-ui and sql-query-webui share their server helpers and web components. The applications run their servers with Node's type stripping and build their web code with Vite. They depend on the published package (ADR-2), and a change of the toolkit is tried in them before it is released.
Decision: The source is .ts and .tsx files with three entry points: ./server, ./web and
./vite. While the toolkit and an application change together, npm link --no-save replaces the
installed package with the checkout, leaving package.json and the lock file on the published
version; Node runs the server helpers from the checkout's real path, where type stripping
applies, and Vite compiles the web code. The server helpers import only Node's modules at run
time, and types of Fastify; the Vite helpers import only types of Vite, taking loadEnv's answer
from the application. The web code's React, MUI and Emotion come from the application, which
dedupes them with TOOLKIT_RESOLVE. The peer dependencies are optional, as an application uses
only some of the entry points, and are development dependencies for the toolkit's own checks.
Consequences: The dependency of an application never names a path; npm ci replaces a link
with the published version. Node refuses type stripping in node_modules, so the published
package is compiled (ADR-2).
ADR-2: The published package is built in dist/ and published to Gitea by a tag
Status: Accepted
Context: An installed package lives in node_modules, where Node does not strip types, so the
published package has to be JavaScript, while the linked checkout stays TypeScript source. The
repository is on Gitea, which has an npm registry per owner and Actions mostly compatible with
GitHub's.
Decision: build.mjs compiles the toolkit with tsconfig.build.json, which rewrites the .ts
import extensions to .js, into dist/ with declarations, and writes dist/package.json with
exports of the compiled files and declarations; the package published is dist/, while the
source package.json stays private with exports of the source. The name is
@koszewscy/sk-react-toolkit, scoped to the owner, so an application takes only this scope from
Gitea; it depends on a published version and imports the package by this name, which is also the
name npm link gives the checkout. The package is built and published only by the
Release workflow, which follows linux-cloud-router's: it runs for each tag and on demand, with
the secrets CI_URL, CI_OWNER and CI_TOKEN, and skips a version already published.
actions/setup-node gives npm the registry of the package's scope and the token, so the package
carries no registry; release.mjs refuses a tag x.y.z that is not the version of
package.json.
Consequences: A release raises the version in both CHANGELOG.md and package.json, and a
re-run, or a tag x.y.z-n, publishes nothing. The declarations of renderApp reference the
declaration of the virtual:font module, which build.mjs copies into dist/.
ADR-3: Credentials of any kind are kept in config.json, with secrets encrypted by a key of the environment
Status: Accepted
Context: sql-query-webui kept PostgreSQL connection profiles with encrypted passwords, and manage-ui needs a PostgreSQL connection and an Azure service principal without taking them from its environment. Other systems will need other kinds of credentials.
Decision: A credential store is a list in a key of the application's config.json, all of one
kind; each entry has a name and the fields of the store's kind, and no kind of its own, as the key
of config.json, whose schema the application defines, already says what the entries are. An
application with several kinds of credentials has a store, a key, for each. The application gives
the kind as data: each field with its key, label, help, type (text, port, GUID or a choice),
whether it is required and whether it is secret. The toolkit checks the entries, serves read,
validate and write routes, and renders the dialog from the kind, so a new kind needs no new code;
the dialog edits the stores of one config.json in tabs, and its Save writes them one after the
other, each with the version the previous write answers. Secret fields are encrypted with
AES-256-GCM, a random 12-byte IV each, with the key the SHA-256 of ENCRYPTION_KEY, else of the
content of ENCRYPTION_KEY_FILE. A secret is never answered: read gives null and whether it is
kept, and a write sends null to keep it, '' to drop it, or a new secret. The applications decide
how a credential is used: credentialValues answers it with its secrets decrypted. The types the
server and the dialog share are in shared/credentials.ts, exported by both entry points.
Consequences: Without the key no secret can be saved or used, and a changed key makes the saved secrets unusable until they are entered again. SHA-256 does not slow down guessing a weak key, so the key has to be long and random. Anyone who reaches the routes can replace credentials, as the applications have no authentication.
ADR-4: The key of a credential store is a path, so related stores share one object
Status: Accepted
Context: sql-query-webui keeps connection profiles of PostgreSQL and of SQL Server. As stores of
top-level keys, the keys have to carry the kind in their names, and a key such as connections
does not say which kind it holds once there are several.
Decision: The key of a store is a key of config.json or a path of keys separated by dots, such
as connections.pg and connections.mssql. The store reads the list at the path, reports its
problems by <path>.<index>.<field>, and a write sets the list at the path, creating the objects
of the path that are missing, or are not objects, and keeping their other keys, so stores of one
object can be written one after the other. The kind stays the store's, not a field of an entry
(ADR-3).
Consequences: A key of config.json holding a dot cannot be the key of a store. A single key
reads and writes as before, so applications of one-key stores need no change. The application
checks the object of a path, such as the keys it allows in it, itself.
ADR-5: Terraform modules, instances and their variable files are tables shared through the toolkit
Status: Accepted
Context: Cloud Platform deploys many instances of many Terraform modules, and each module's configurator, a separate application, edits the variables of its instances. Both have to read and write the same modules, instances and variable files, with the same versions, so that a write from an outdated read is refused whichever application made it.
Decision: The toolkit holds the tables and their functions and procedures,
server/sql/tfvars_store.sql, and createTfvarsStore, which both applications use with their own
pg pool. An instance names the schema of its state, unique among instances. A variable file is
a row of its instance, its object as jsonb with the one it replaced. The version of an
instance's files is the name and the SHA-256 of each file's text, tfvarsText of its object, in
name order; a write locks the table, compares the version and writes in one transaction. A change
the tables refuse is an HttpError with the status the routes answer. pg is an optional peer
dependency, as the applications that do not use the store do not need it.
Consequences: Every application of the store works on the same rows and refuses the same
conflicts, and changes of the tables reach them with a release of the toolkit. The objects' keys
are in jsonb's order. The store is tested against a real PostgreSQL server, which the release
workflow does not have, so npm test runs on the developer's machine.