Files
sk-react-toolkit/README.md
T

8.9 KiB

sk-react-toolkit

The shared code of the web tools built as a Fastify server in TypeScript and a React application with MUI built by Vite, such as azure-platform's manage-ui and sql-query-webui. It is TypeScript source: Node 26 runs the server helpers, and the application's Vite build compiles the web code.

Using it

The package is @koszewscy/sk-react-toolkit, published to the npm registry of Gitea, https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/. An application depends on a published version and imports @koszewscy/sk-react-toolkit/server, /web and /vite. Its .npmrc names the registry of the scope:

@koszewscy:registry=https://gitea.koszewscy.waw.pl/api/packages/koszewscy/npm/
{ "dependencies": { "@koszewscy/sk-react-toolkit": "^0.2.0" } }

The web application adds the toolkit's resolve options and font plugin to its vite.config.ts, so React, MUI and Emotion come from its own node_modules:

import { font, fontChoice, TOOLKIT_RESOLVE } from '@koszewscy/sk-react-toolkit/vite'
import { defineConfig, loadEnv } from 'vite'

export default defineConfig(({ mode }) => ({
  plugins: [react(), font(fontChoice(loadEnv(mode, import.meta.dirname)))],
  resolve: TOOLKIT_RESOLVE,
}))

To change the toolkit and an application together, npm link --no-save replaces the installed package with the checkout, the TypeScript source, in each package of the application:

npm link --no-save ../sk-react-toolkit
npm --prefix web link --no-save ../../sk-react-toolkit

package.json and the lock file keep the published version; npm ci or npm install replaces the link with it again. A change of the toolkit reaches the application through a release of the toolkit and an update of the dependency.

The peer dependencies are the application's: fastify and @fastify/static for the server helpers; react, react-dom, @mui/material, @mui/icons-material and Emotion for the web code; vite for the Vite helpers.

sk-react-toolkit/server

Export Purpose
HttpError an error answered with its status and { error }, with problems and a detail message for a refused write
strict(properties) the JSON schema of an object with exactly these properties, all required
errorHandler(log) Fastify's error handler: { error }, or { error, problems, message }; unexpected 5xx errors logged with their stack
notFoundHandler(webDir, buildHint) 404 for /api paths, index.html for the others, 503 with the hint while the frontend is not built
createLog, timestampsOn, QUIET, Log the log of a server, with optional timestamps and requests; QUIET for tests
fail(message) prints the error in red and ends the process with status 2
gitVersion(dir, variable) git describe of the checkout, else the environment variable, else dev
exitOnSignals(close) closes the server on Ctrl-C and SIGTERM, ending the process after 5 seconds at most
readJsonObject(path, options) a JSON object file and its version, the SHA-256 of its bytes; optionally {} for a missing file
encryptionKey(), encrypt(text, key), decrypt(encrypted, key) the key of the secrets and AES-256-GCM encryption; see Credential store
CredentialStore, credentialProblems, credentialValues, registerCredentialRoutes, POSTGRESQL_KIND the credential store; see Credential store

sk-react-toolkit/web

Export Purpose
request, ApiError, Validation a JSON call to the backend, its error with the problems of a refused write, and the answer of a validate endpoint
appTheme(colorSchemes?), renderApp(options) the theme (light, dark and system, Regular font, small text fields) and the startup that retries the first call while showing Connecting
REGULAR, MONOSPACED, verbatim the font styles and the attributes of inputs the browser must not change
Card a centered panel with a title and a message
ThemeMenu the button cycling light, dark and system theme
SwitcherMenu, SwitcherItem a button naming the item chosen, whose menu chooses another, such as a page or a connection
TextInput a text field with clear and show/hide buttons
RowDialog, useValidation a dialog that edits a value validated by the server as it changes, and the validation hook
createBrowserSettings(keys, event) switches kept in localStorage and followed across tabs
CredentialsButton the dialog of a credential store; see Credential store

sk-react-toolkit/vite

Export Purpose
font(choice), fontChoice(env), FONTS the font plugin of VITE_FONT: cdn (Google Fonts links, the default), bundled (@fontsource/roboto, a dependency of the application) or none
TOOLKIT_RESOLVE the resolve options that keep one copy of React, MUI and Emotion

Credential store

A credential store is a list of credentials in a key of the application's config.json, of the kinds the application gives, such as a database connection or a service principal. Each entry has a name, unique and not one of the store's reserved names, a kind, and the fields of its kind:

{
  "credentials": [
    {
      "name": "state",
      "kind": "postgresql",
      "host": "db.example.com",
      "port": 5432,
      "database": "platform",
      "user": "tofu",
      "ssl_mode": "verify-full",
      "password": "<encrypted>"
    }
  ]
}

A kind (CredentialKind) names its fields (CredentialField), each by its key in config.json, with a label, a help text, a type, text, port, guid or choice with its choices, whether it is required, and whether it is secret. POSTGRESQL_KIND is the kind of a PostgreSQL connection: host, port, database, user, ssl_mode (disable, require or verify-full) and a secret password.

A secret field is encrypted with AES-256-GCM, stored as base64 of a random IV, the ciphertext and the authentication tag. The key is the SHA-256 of ENCRYPTION_KEY, else of the content of the file ENCRYPTION_KEY_FILE names, without its final newline. Without either, no secret can be saved or used; a secret encrypted with another key cannot be decrypted and has to be entered again.

CredentialStore describes a store: the path of config.json, the key, the kinds, the reserved names and whether a missing config.json reads as {}.

  • credentialProblems(value, store): the problems of the key's value by <key>.<index>.<field>, for the application's own check of config.json.
  • credentialValues(store, name): a credential with its secrets decrypted; HttpError 404 for a name that does not exist, 400 without a key or for a secret the key does not decrypt.
  • registerCredentialRoutes(app, store, prefix, log): GET <prefix>/read, POST <prefix>/validate and POST <prefix>/write. Read answers the credentials, CredentialProfile, with values (the fields that are not secret, as text), secrets (null for each secret, never answered) and stored (which secrets are kept), the kinds, the reserved names, the path of config.json, its version and whether the server has a key. A write takes the version read and the credentials; a secret is null to keep the secret of the credential named original, '' for none, or the new secret, which is encrypted. A file that changed since the version read is 409, credentials with problems 422; the other keys of config.json are kept.

CredentialsButton (prefix, title, icon, note, onSaved) opens the store's credentials in a dialog: a list at the left with Add for each kind and Remove, and the fields of the credential selected, as its kind names them. A secret shows only whether it is kept: Set or Change replaces it and Remove drops it.

Development

npm install installs the peer dependencies as development dependencies; npm run typecheck and npm run lint check the code. npm run build (build.mjs) builds the published package in dist/: the code compiled to JavaScript with its declarations, README.md, CHANGELOG.md and a package.json whose exports name the compiled files.

Releases

./release.mjs commits, tags the version of the newest CHANGELOG.md entry and pushes main with the tag; -n prints the git commands instead. The version of a release of the code, x.y.z, is that of package.json, so a release raises both. A tag x.y.z-n releases changes that are not code changes; its version is already published, so its run publishes nothing.

The Release workflow, .gitea/workflows/release.yml, builds and publishes the package on Gitea Actions for each tag and on demand: it installs the dependencies, typechecks and lints the code, builds the package and publishes dist/ to the npm registry of the owner CI_OWNER on the server CI_URL, with CI_TOKEN, a token with the package scope; all three are secrets of the repository. A version already published is skipped, as the registry refuses the same version again.