Files
website/db/reorg/07_triggers_meta_grants.sql
Stan Runge 64fb8df262 db: Drizzle schemas on domain Postgres schemas, baseline migration, reorg SQL
- pgSchema tables for auth, money, house, ref, games, cs, animaldex, site, meta
  (lists excluded via schemaFilter; runtime list tables are managed by meta.*)
- drizzle/ baseline matches the reorged DB exactly (catalog diff = 0 on rehearsal)
- db/reorg: one-time SQL that moves the flat public schema (rehearsed on a prod copy)
- scripts/db-migrate.ts: migrator with --init to record the baseline and a guard
  against running the baseline on a non-empty, unrecorded database
2026-10-23 22:03:42 +00:00

80 lines
5.1 KiB
SQL

-- 07_triggers_meta_grants.sql: updated_at triggers everywhere, register core tables in meta, grants.
SET ROLE personal;
-- 1) updated_at trigger on every table that has the column but no trigger (27 pre-existing + new ones)
DO $$
DECLARE r record; n integer := 0;
BEGIN
FOR r IN
SELECT n.nspname AS s, c.relname AS t
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
JOIN pg_attribute a ON a.attrelid = c.oid AND a.attname = 'updated_at' AND NOT a.attisdropped
WHERE c.relkind = 'r'
AND n.nspname IN ('auth','money','house','ref','games','cs','animaldex','site','hardware','energy')
AND NOT EXISTS (SELECT 1 FROM pg_trigger tg WHERE tg.tgrelid = c.oid AND NOT tg.tgisinternal
AND tg.tgfoid = 'meta.touch_updated_at()'::regprocedure)
LOOP
EXECUTE format('CREATE TRIGGER touch_updated_at BEFORE UPDATE ON %I.%I FOR EACH ROW EXECUTE FUNCTION meta.touch_updated_at()', r.s, r.t);
n := n + 1;
END LOOP;
RAISE NOTICE 'added updated_at triggers: %', n;
END $$;
-- the 16 old triggers were all named site_touch_updated_at; normalise the name
DO $$
DECLARE r record;
BEGIN
FOR r IN SELECT tg.tgname, tg.tgrelid::regclass AS rel FROM pg_trigger tg WHERE tg.tgname = 'site_touch_updated_at' LOOP
EXECUTE format('ALTER TRIGGER site_touch_updated_at ON %s RENAME TO touch_updated_at', r.rel);
END LOOP;
END $$;
RESET ROLE;
-- 2) register core + ref tables in meta (GUI visibility); auth + bookkeeping are not exposed
INSERT INTO meta.tables (schema_name, table_name, slug, display_name, kind, editable, has_done, sort_order)
SELECT n.nspname, c.relname,
replace(n.nspname || '-' || c.relname, '_', '-'),
initcap(replace(c.relname, '_', ' ')),
CASE WHEN n.nspname = 'ref' THEN 'ref' ELSE 'core' END,
n.nspname <> 'ref', false, 1000
FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('r','v')
AND n.nspname IN ('money','house','ref','games','cs','animaldex','site','hardware','energy')
ON CONFLICT DO NOTHING;
UPDATE meta.tables SET editable = false WHERE kind = 'core' AND (schema_name, table_name) IN
(SELECT n.nspname, c.relname FROM pg_class c JOIN pg_namespace n ON n.oid = c.relnamespace WHERE c.relkind = 'v');
SELECT meta.sync();
-- 3) grants
GRANT USAGE ON SCHEMA auth, money, house, ref, games, cs, animaldex, site, meta, hardware, energy, lists TO personal_app, personal_ro;
-- app: full DML on its own data, read-only on ref
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy, lists TO personal_app;
GRANT SELECT ON ALL TABLES IN SCHEMA ref, meta TO personal_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy, lists TO personal_app;
-- app may edit presentation fields of the registry directly
GRANT UPDATE (slug, display_name, description, icon, picture_url, default_sort, sort_order) ON meta.tables TO personal_app; -- slug: URL only, table name stays
GRANT UPDATE (label, ui_type, options, position, hidden, help_text) ON meta.columns TO personal_app;
GRANT EXECUTE ON FUNCTION meta.create_list(text, text, text, boolean, jsonb, text, text, text, integer),
meta.add_column(text, jsonb), meta.rename_column(text, text, text), meta.drop_column(text, text),
meta.archive_list(text), meta.drop_list(text), meta.sync(integer) TO personal_app;
-- ro: everything readable
GRANT SELECT ON ALL TABLES IN SCHEMA auth, money, house, ref, games, cs, animaldex, site, meta, hardware, energy, lists TO personal_ro;
-- future objects
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy
GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA auth, money, house, games, cs, animaldex, site, hardware, energy
GRANT USAGE, SELECT ON SEQUENCES TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA ref, meta GRANT SELECT ON TABLES TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal IN SCHEMA auth, money, house, ref, games, cs, animaldex, site, meta, hardware, energy
GRANT SELECT ON TABLES TO personal_ro;
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT, INSERT, UPDATE, DELETE ON TABLES TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT USAGE, SELECT ON SEQUENCES TO personal_app;
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT ON TABLES TO personal_ro;
-- migration owner can read the runtime lists, so deploy backups (pg_dump as `personal`) cover everything
GRANT USAGE ON SCHEMA lists TO personal;
GRANT SELECT ON ALL TABLES IN SCHEMA lists TO personal;
GRANT SELECT ON ALL SEQUENCES IN SCHEMA lists TO personal;
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT ON TABLES TO personal;
ALTER DEFAULT PRIVILEGES FOR ROLE personal_ddl IN SCHEMA lists GRANT SELECT ON SEQUENCES TO personal;
-- nobody creates in public anymore
REVOKE CREATE ON SCHEMA public FROM PUBLIC;