138 lines
8.7 KiB
Markdown
138 lines
8.7 KiB
Markdown
# 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.
|
|
|
|
## ADR-6: A fourth entry point, /shared, exports the shared types and constants
|
|
|
|
Status: Accepted
|
|
|
|
Context: An application keeps the types of its API in one file that its server and its web
|
|
application both import. When those types name types of the toolkit, such as `TfModule`, the file
|
|
must import them from an entry point both sides can resolve: `/server` brings Fastify and `pg`
|
|
into the web application's type check, `/web` brings React and MUI into the server's.
|
|
|
|
Decision: `shared/index.ts` is the entry point `/shared`, built and exported like the other three.
|
|
It exports the types and constants of `shared/`, the credential types and the types and name
|
|
patterns of the tfvars store, and no code that needs a peer dependency. `/server` and `/web` keep
|
|
exporting them too.
|
|
|
|
Consequences: An application's shared API types import from `/shared` without the code of either
|
|
side. A type added to `shared/` is exported from `/shared` as well as from the side that uses it.
|