1 Commits
Author SHA1 Message Date
slawek 959d578910 fix: a module of the tfvars store says whether it discovers its configurator, as configuratorDiscovery.
Release / release (push) Successful in 25s
2026-11-07 02:03:17 +01:00
8 changed files with 86 additions and 24 deletions

No files matched your search

+10
View File
@@ -1,5 +1,15 @@
# Changelog
## 0.7.2
fix: a module of the tfvars store says whether it discovers its configurator, as
configuratorDiscovery.
- `TfModule` has `configuratorDiscovery`, and `tf_modules` the column `configurator_discovery`:
whether a module without `configurator_url` finds its configurator by the `config.json` of its
folder or one above it. A module registered before keeps it on; `modules.put` sets it.
- `tf_module_put` takes the new argument; its earlier signature is dropped.
## 0.7.1
fix: a page in the dark color scheme, also one in an iframe, is no longer white while it loads,
+1 -1
View File
@@ -178,7 +178,7 @@ which edit their variables. `createTfvarsStore(pool)` takes a `pg` pool and appl
| Table | Rows |
| --- | --- |
| `tf_modules` | `name`, `root`, the folder of the Terraform code, `description` and `configurator_url` |
| `tf_modules` | `name`, `root`, the folder of the Terraform code, `description`, `configurator_url` and `configurator_discovery`, whether a module without `configurator_url` finds its configurator by the `config.json` of its folder or one above it; a module registered before the column keeps it on, a new one has it off unless given |
| `tf_instances` | `module`, `name` and `state_schema`, the schema of the instance's state in the pg backend, unique, and `created` |
| `tf_variable_files` | `module`, `instance`, `name` (`*.tfvars.json`), `content` and `previous`, the object it replaced, as `jsonb`, and `updated` |
| `tf_variable_revisions` | `module`, `instance`, `revision`, numbered from 1 for each instance, `files`, all its files after a write as a `jsonb` object by name, `schema`, the schema version of their layout or null, `producer` and `saved` |
+2 -2
View File
@@ -1,12 +1,12 @@
{
"name": "@koszewscy/sk-react-toolkit",
"version": "0.7.1",
"version": "0.7.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@koszewscy/sk-react-toolkit",
"version": "0.7.1",
"version": "0.7.2",
"devDependencies": {
"@emotion/react": "^11.14.0",
"@emotion/styled": "^11.14.1",
+1 -1
View File
@@ -1,6 +1,6 @@
{
"name": "@koszewscy/sk-react-toolkit",
"version": "0.7.1",
"version": "0.7.2",
"description": "Shared code of the React and Fastify web applications: web components, server helpers and the Vite font and color scheme plugins.",
"private": true,
"type": "module",
+36 -11
View File
@@ -4,12 +4,24 @@
-- again.
CREATE TABLE IF NOT EXISTS tf_modules (
name text PRIMARY KEY CHECK (name ~ '^[a-z][a-z0-9-]*$'),
root text NOT NULL CHECK (root <> ''),
description text NOT NULL DEFAULT '',
configurator_url text
name text PRIMARY KEY CHECK (name ~ '^[a-z][a-z0-9-]*$'),
root text NOT NULL CHECK (root <> ''),
description text NOT NULL DEFAULT '',
configurator_url text,
configurator_discovery boolean NOT NULL DEFAULT false
);
-- A module registered before configurator_discovery keeps finding its configurator.
DO $$
BEGIN
IF NOT EXISTS (SELECT 1 FROM information_schema.columns
WHERE table_schema = current_schema() AND table_name = 'tf_modules' AND column_name = 'configurator_discovery') THEN
ALTER TABLE tf_modules ADD COLUMN configurator_discovery boolean NOT NULL DEFAULT true;
ALTER TABLE tf_modules ALTER COLUMN configurator_discovery SET DEFAULT false;
END IF;
END
$$;
CREATE TABLE IF NOT EXISTS tf_instances (
module text NOT NULL REFERENCES tf_modules (name),
name text NOT NULL CHECK (name ~ '^[a-z][a-z0-9-]*$'),
@@ -51,23 +63,36 @@ WHERE NOT EXISTS (SELECT 1 FROM tf_variable_revisions r WHERE r.module = f.modul
GROUP BY f.module, f.instance
ON CONFLICT DO NOTHING;
-- The modules, by name.
-- The modules, by name; its columns are those of the table, so a table of other columns replaces it.
DO $$
BEGIN
IF EXISTS (SELECT 1 FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace
WHERE n.nspname = current_schema() AND p.proname = 'tf_modules_list'
AND NOT 'configurator_discovery' = ANY (coalesce(p.proargnames, '{}'))) THEN
DROP FUNCTION tf_modules_list();
END IF;
END
$$;
CREATE OR REPLACE FUNCTION tf_modules_list()
RETURNS TABLE (name text, root text, description text, configurator_url text)
RETURNS TABLE (name text, root text, description text, configurator_url text, configurator_discovery boolean)
LANGUAGE sql STABLE
SET search_path FROM CURRENT
AS $$
SELECT m.name, m.root, m.description, m.configurator_url FROM tf_modules m ORDER BY m.name
SELECT m.name, m.root, m.description, m.configurator_url, m.configurator_discovery FROM tf_modules m ORDER BY m.name
$$;
DROP PROCEDURE IF EXISTS tf_module_put(text, text, text, text);
-- Creates a module, or changes the one of that name.
CREATE OR REPLACE PROCEDURE tf_module_put(module_name text, module_root text, module_description text, module_configurator_url text)
CREATE OR REPLACE PROCEDURE tf_module_put(module_name text, module_root text, module_description text, module_configurator_url text, module_configurator_discovery boolean)
LANGUAGE sql
SET search_path FROM CURRENT
AS $$
INSERT INTO tf_modules (name, root, description, configurator_url)
VALUES (module_name, module_root, module_description, module_configurator_url)
ON CONFLICT (name) DO UPDATE SET root = excluded.root, description = excluded.description, configurator_url = excluded.configurator_url
INSERT INTO tf_modules (name, root, description, configurator_url, configurator_discovery)
VALUES (module_name, module_root, module_description, module_configurator_url, module_configurator_discovery)
ON CONFLICT (name) DO UPDATE SET root = excluded.root, description = excluded.description,
configurator_url = excluded.configurator_url, configurator_discovery = excluded.configurator_discovery
$$;
-- Deletes a module; the foreign key of tf_instances refuses it while the module has instances.
+28 -6
View File
@@ -35,16 +35,38 @@ const status = async (promise: Promise<unknown>) => {
}
test('modules are created, changed, listed and deleted', async () => {
await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: null })
await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/' })
await store.modules.put({ name: 'app', root: 'app', description: '', configuratorUrl: null })
await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: null, configuratorDiscovery: true })
await store.modules.put({ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/', configuratorDiscovery: false })
await store.modules.put({ name: 'app', root: 'app', description: '', configuratorUrl: null, configuratorDiscovery: true })
assert.deepEqual(await store.modules.list(), [
{ name: 'app', root: 'app', description: '', configuratorUrl: null },
{ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/' },
{ name: 'app', root: 'app', description: '', configuratorUrl: null, configuratorDiscovery: true },
{ name: 'router', root: 'router/terraform', description: 'VPN router', configuratorUrl: 'http://localhost:7083/', configuratorDiscovery: false },
])
await store.modules.delete('app')
assert.deepEqual((await store.modules.list()).map((module) => module.name), ['router'])
assert.equal(await status(store.modules.put({ name: 'Bad', root: 'x', description: '', configuratorUrl: null })), 400)
assert.equal(await status(store.modules.put({ name: 'Bad', root: 'x', description: '', configuratorUrl: null, configuratorDiscovery: false })), 400)
})
test('a module registered before configurator_discovery keeps discovering its configurator; a new one does not', async () => {
// The table, the function and the procedure as they were before the column, in a schema of their own.
await pool.query(`CREATE SCHEMA earlier;
CREATE TABLE earlier.tf_modules (name text PRIMARY KEY, root text NOT NULL, description text NOT NULL DEFAULT '', configurator_url text);
INSERT INTO earlier.tf_modules VALUES ('router', 'router/terraform', '', NULL);
CREATE FUNCTION earlier.tf_modules_list() RETURNS TABLE (name text, root text, description text, configurator_url text)
LANGUAGE sql AS $$ SELECT m.name, m.root, m.description, m.configurator_url FROM earlier.tf_modules m $$;
CREATE PROCEDURE earlier.tf_module_put(a text, b text, c text, d text) LANGUAGE sql AS $$ SELECT 1 $$;`)
const earlierPool = new pg.Pool({ database, options: '-c search_path=earlier' })
try {
const earlier = createTfvarsStore(earlierPool)
await earlier.modules.put({ name: 'app', root: 'app', description: '', configuratorUrl: null, configuratorDiscovery: false })
assert.deepEqual((await earlier.modules.list()).map((module) => [module.name, module.configuratorDiscovery]), [['app', false], ['router', true]])
const { rows } = await earlierPool.query("SELECT count(*)::int AS count FROM pg_proc p JOIN pg_namespace n ON n.oid = p.pronamespace WHERE n.nspname = 'earlier' AND p.proname = 'tf_module_put'")
assert.equal(rows[0].count, 1)
// Another store applies the file again over the migrated schema, which keeps the modules as they are.
assert.deepEqual((await createTfvarsStore(earlierPool).modules.list()).map((module) => [module.name, module.configuratorDiscovery]), [['app', false], ['router', true]])
} finally {
await earlierPool.end()
}
})
test('instances are created and refused for a missing module or a name in use', async () => {
+3 -3
View File
@@ -152,10 +152,10 @@ export function createTfvarsStore(pool: Pool): TfvarsStore {
}
return {
modules: {
list: async () => (await query<{ name: string; root: string; description: string; configurator_url: string | null }>('SELECT * FROM tf_modules_list()', []))
.map((row) => ({ name: row.name, root: row.root, description: row.description, configuratorUrl: row.configurator_url })),
list: async () => (await query<{ name: string; root: string; description: string; configurator_url: string | null; configurator_discovery: boolean }>('SELECT * FROM tf_modules_list()', []))
.map((row) => ({ name: row.name, root: row.root, description: row.description, configuratorUrl: row.configurator_url, configuratorDiscovery: row.configurator_discovery })),
async put(module) {
await query('CALL tf_module_put($1, $2, $3, $4)', [module.name, module.root, module.description, module.configuratorUrl])
await query('CALL tf_module_put($1, $2, $3, $4, $5)', [module.name, module.root, module.description, module.configuratorUrl, module.configuratorDiscovery])
},
async delete(name) {
await query('CALL tf_module_delete($1)', [name])
+5
View File
@@ -5,6 +5,11 @@ export type TfModule = {
description: string
/** The address of the module's configurator, the application that edits the variables of its instances. */
configuratorUrl: string | null
/**
* Whether a module without configuratorUrl finds its configurator by the config.json of its folder or one above it;
* without either, the module has no configurator.
*/
configuratorDiscovery: boolean
}
/** A deployment of a module, whose state is kept in the schema stateSchema of the pg backend. */