From c43de66497c00c7e946c1a121c390635e1a371e4 Mon Sep 17 00:00:00 2001 From: Gefei Date: Sun, 21 Jun 2026 23:52:45 -0700 Subject: [PATCH 1/2] Sync constructive-db extension --- packages/metaschema-schema/Makefile | 2 +- .../fixtures/node_type_registry_seed.sql | 30 +++++++++-- .../metaschema_public/tables/schema/table.sql | 3 ++ .../triggers/enforce_api_exposure_ratchet.sql | 29 ++++++++++ .../tables/view_table/table.sql | 4 ++ .../types/api_exposure_level.sql | 13 +++++ .../types/object_category.sql | 5 +- .../metaschema-schema.control | 2 +- packages/metaschema-schema/package.json | 2 +- packages/metaschema-schema/pgpm.plan | 4 +- .../triggers/enforce_api_exposure_ratchet.sql | 8 +++ .../types/api_exposure_level.sql | 7 +++ ...26.5.sql => metaschema-schema--0.29.0.sql} | 54 +++++++++++++++++-- .../triggers/enforce_api_exposure_ratchet.sql | 10 ++++ .../tables/view_table/table.sql | 2 +- .../types/api_exposure_level.sql | 9 ++++ 16 files changed, 171 insertions(+), 13 deletions(-) create mode 100644 packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql create mode 100644 packages/metaschema-schema/deploy/schemas/metaschema_public/types/api_exposure_level.sql create mode 100644 packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql create mode 100644 packages/metaschema-schema/revert/schemas/metaschema_public/types/api_exposure_level.sql rename packages/metaschema-schema/sql/{metaschema-schema--0.26.5.sql => metaschema-schema--0.29.0.sql} (96%) create mode 100644 packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql create mode 100644 packages/metaschema-schema/verify/schemas/metaschema_public/types/api_exposure_level.sql diff --git a/packages/metaschema-schema/Makefile b/packages/metaschema-schema/Makefile index d943ef3d..86d92dda 100644 --- a/packages/metaschema-schema/Makefile +++ b/packages/metaschema-schema/Makefile @@ -1,5 +1,5 @@ EXTENSION = metaschema-schema -DATA = sql/metaschema-schema--0.26.5.sql +DATA = sql/metaschema-schema--0.29.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql index 05361345..e661c978 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -3,7 +3,7 @@ -- GENERATED FILE — DO NOT EDIT -- Regenerate with: cd packages/node-type-registry && pnpm generate -- --- Node types: 78 +-- Node types: 79 -- requires: schemas/metaschema_public/schema -- requires: schemas/metaschema_public/tables/node_type_registry/table @@ -191,7 +191,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'authz', 'File Path Share', 'Path-scoped file sharing via ltree containment. Grants access when a path_shares row matches the current user, bucket, and an ancestor path with the required permission.', - '{"type":"object","properties":{"shares_schema":{"type":"string","description":"Schema of the path_shares table"},"shares_table":{"type":"string","description":"Name of the path_shares table"},"files_schema":{"type":"string","description":"Schema of the files table (used to qualify column references inside the EXISTS subquery)"},"files_table":{"type":"string","description":"Name of the files table (used to qualify column references inside the EXISTS subquery)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["shares_schema","shares_table","files_table","permission_field"]}'::jsonb, + '{"type":"object","properties":{"shares_table_id":{"type":"string","format":"uuid","description":"UUID of the path_shares table (alternative to shares_schema/shares_table)"},"shares_schema":{"type":"string","description":"Schema of the path_shares table (or use shares_table_id)"},"shares_table":{"type":"string","description":"Name of the path_shares table (or use shares_table_id)"},"files_table_id":{"type":"string","format":"uuid","description":"UUID of the files table (alternative to files_schema/files_table)"},"files_schema":{"type":"string","description":"Schema of the files table (or use files_table_id)"},"files_table":{"type":"string","description":"Name of the files table (or use files_table_id)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["permission_field"]}'::jsonb, '{"storage","authz"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -383,7 +383,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'authz', 'Related Member List', 'Array membership check in a related table.', - '{"type":"object","properties":{"owned_schema":{"type":"string","description":"Schema of the related table"},"owned_table":{"type":"string","description":"Name of the related table"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_schema","owned_table","owned_table_key","owned_table_ref_key","this_object_key"]}'::jsonb, + '{"type":"object","properties":{"owned_table_id":{"type":"string","format":"uuid","description":"UUID of the related table (alternative to owned_schema/owned_table)"},"owned_schema":{"type":"string","description":"Schema of the related table (or use owned_table_id)"},"owned_table":{"type":"string","description":"Name of the related table (or use owned_table_id)"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_table_key","owned_table_ref_key","this_object_key"]}'::jsonb, '{"ownership","authz"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -1137,6 +1137,30 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema = EXCLUDED.parameter_schema, tags = EXCLUDED.tags; +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'GuardStepUp', + 'guard_step_up', + 'guard', + 'Guard Step-Up', + 'Attaches a BEFORE trigger that calls require_step_up() to enforce recent password or MFA verification before allowing mutations. Requires a provisioned sessions_module (with app_settings_auth) for the target database. The step_up_window is read from app_settings_auth at runtime (default 30 minutes). Supports compound conditions (AND/OR/NOT), watch_fields (fire only when specific fields change), and simple condition_field/condition_value leaf conditions.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"step_up_type":{"type":"string","enum":["password","mfa","password_or_mfa"],"description":"Which verification method satisfies the step-up requirement","default":"password_or_mfa"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Which DML events require step-up verification","default":["UPDATE","DELETE"]},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":[]}'::jsonb, + '{"guard","triggers","auth","step-up","mfa","security"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + INSERT INTO metaschema_public.node_type_registry ( name, slug, diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/table.sql index 50e31ae5..f4767c83 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/table.sql @@ -3,6 +3,7 @@ -- requires: schemas/metaschema_public/schema -- requires: schemas/metaschema_public/tables/database/table -- requires: schemas/metaschema_public/types/object_category +-- requires: schemas/metaschema_public/types/api_exposure_level BEGIN; @@ -24,6 +25,8 @@ CREATE TABLE metaschema_public.schema ( is_public boolean NOT NULL DEFAULT TRUE, + api_exposure metaschema_public.api_exposure_level NOT NULL DEFAULT 'exposable', + created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now(), diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql new file mode 100644 index 00000000..526ef9ba --- /dev/null +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql @@ -0,0 +1,29 @@ +-- Deploy schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet to pg + +-- requires: schemas/metaschema_public/tables/schema/table + +BEGIN; + +-- never_expose is a one-way ratchet: once set, it cannot be changed via the app. +-- Loosening internal_only → exposable is governed by introspection-layer permissions. +CREATE FUNCTION metaschema_public.tg_enforce_api_exposure_ratchet() +RETURNS TRIGGER AS $$ +BEGIN + IF OLD.api_exposure = 'never_expose' THEN + RAISE EXCEPTION 'Cannot change api_exposure from ''never_expose'' on schema "%". This level is permanent and can only be removed via a direct database migration.', + OLD.name + USING ERRCODE = 'check_violation'; + END IF; + + RETURN NEW; +END; +$$ +LANGUAGE plpgsql STABLE; + +CREATE TRIGGER _000003_enforce_api_exposure_ratchet +BEFORE UPDATE ON metaschema_public.schema +FOR EACH ROW +WHEN (NEW.api_exposure IS DISTINCT FROM OLD.api_exposure) +EXECUTE FUNCTION metaschema_public.tg_enforce_api_exposure_ratchet(); + +COMMIT; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/view_table/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/view_table/table.sql index 6992e061..2e065e53 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/view_table/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/view_table/table.sql @@ -3,6 +3,7 @@ -- requires: schemas/metaschema_public/schema -- requires: schemas/metaschema_public/tables/view/table -- requires: schemas/metaschema_public/tables/table/table +-- requires: schemas/metaschema_public/tables/database/table BEGIN; @@ -11,12 +12,14 @@ BEGIN; -- The primary table is stored in view.table_id; this table stores additional joined tables. CREATE TABLE metaschema_public.view_table ( id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), view_id uuid NOT NULL, table_id uuid NOT NULL, -- Order of joins (0 = first join, 1 = second join, etc.) join_order int NOT NULL DEFAULT 0, + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT view_fkey FOREIGN KEY (view_id) REFERENCES metaschema_public.view (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, @@ -25,6 +28,7 @@ CREATE TABLE metaschema_public.view_table ( COMMENT ON TABLE metaschema_public.view_table IS 'Junction table linking views to their joined tables for referential integrity'; +CREATE INDEX view_table_database_id_idx ON metaschema_public.view_table ( database_id ); CREATE INDEX view_table_view_id_idx ON metaschema_public.view_table ( view_id ); CREATE INDEX view_table_table_id_idx ON metaschema_public.view_table ( table_id ); diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/types/api_exposure_level.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/types/api_exposure_level.sql new file mode 100644 index 00000000..cebd2eb9 --- /dev/null +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/types/api_exposure_level.sql @@ -0,0 +1,13 @@ +-- Deploy schemas/metaschema_public/types/api_exposure_level to pg + +-- requires: schemas/metaschema_public/schema + +BEGIN; + +-- Controls whether a schema can be linked to a public API. +-- 'exposable' - default; schema may be added to any API +-- 'internal_only' - schema is server-side only; adding to an API is blocked but can be overridden by a platform admin +-- 'never_expose' - hard block; schema can never be added to an API (one-way ratchet, cannot be loosened) +CREATE TYPE metaschema_public.api_exposure_level AS ENUM ('exposable', 'internal_only', 'never_expose'); + +COMMIT; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/types/object_category.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/types/object_category.sql index 9480d9f5..41399e2e 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/types/object_category.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/types/object_category.sql @@ -7,7 +7,10 @@ BEGIN; -- Unified category type for all metaschema objects (tables, fields, procedures, triggers, indexes, policies, constraints, etc.) -- 'core' - system-level objects (id fields, entity_id, actor_id, etc.) -- 'module' - objects created by modules (users, permissions, memberships, etc.) +-- 'permissions' - permission-framework objects (SPRTs, grants, permission defaults) — excluded from exports via excludeCategories +-- 'auth' - authentication/session objects (sessions, rate limits, identity providers) — excluded from exports via excludeCategories +-- 'memberships' - membership-structure objects (memberships, members, profiles, settings) — excluded from exports via excludeCategories -- 'app' - user-defined application objects -CREATE TYPE metaschema_public.object_category AS ENUM ('core', 'module', 'app'); +CREATE TYPE metaschema_public.object_category AS ENUM ('core', 'module', 'permissions', 'auth', 'memberships', 'app'); COMMIT; diff --git a/packages/metaschema-schema/metaschema-schema.control b/packages/metaschema-schema/metaschema-schema.control index e4cf25c9..73c1223f 100644 --- a/packages/metaschema-schema/metaschema-schema.control +++ b/packages/metaschema-schema/metaschema-schema.control @@ -1,6 +1,6 @@ # metaschema-schema extension comment = 'metaschema-schema extension' -default_version = '0.26.5' +default_version = '0.29.0' module_pathname = '$libdir/metaschema-schema' requires = 'citext,hstore,pgpm-inflection,pgpm-database-jobs,pgpm-types,pgcrypto,plpgsql,postgis,uuid-ossp,pgpm-verify' relocatable = false diff --git a/packages/metaschema-schema/package.json b/packages/metaschema-schema/package.json index c355289a..1f504117 100644 --- a/packages/metaschema-schema/package.json +++ b/packages/metaschema-schema/package.json @@ -1,6 +1,6 @@ { "name": "@pgpm/metaschema-schema", - "version": "0.28.2", + "version": "0.29.0", "description": "Database metadata utilities and introspection functions", "author": "Dan Lynch ", "contributors": [ diff --git a/packages/metaschema-schema/pgpm.plan b/packages/metaschema-schema/pgpm.plan index 3f6c02ea..30d6e86a 100644 --- a/packages/metaschema-schema/pgpm.plan +++ b/packages/metaschema-schema/pgpm.plan @@ -5,8 +5,9 @@ schemas/metaschema_private/schema [pgpm-inflection:schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx pgpm-database-jobs:schemas/app_jobs/triggers/tg_add_job_with_row pgpm-types:schemas/public/domains/url] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_private/schema schemas/metaschema_public/schema 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/schema schemas/metaschema_public/types/object_category [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/types/object_category +schemas/metaschema_public/types/api_exposure_level [schemas/metaschema_public/schema] 2026-06-18T00:00:00Z devin # add api_exposure_level enum type (exposable, internal_only, never_expose) schemas/metaschema_public/tables/database/table [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/database/table -schemas/metaschema_public/tables/schema/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/schema/table +schemas/metaschema_public/tables/schema/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/types/object_category schemas/metaschema_public/types/api_exposure_level] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/schema/table schemas/metaschema_public/tables/table/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/check_constraint/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/check_constraint/table schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx [schemas/metaschema_private/schema schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx @@ -37,3 +38,4 @@ schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_ schemas/metaschema_public/tables/function/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table] 2026-05-09T00:00:00Z devin # add metaschema_public.function table for tracking generated SQL functions schemas/metaschema_public/tables/partition/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table] 2026-05-26T00:00:00Z Constructive # add metaschema_public.partition table for pg_partman lifecycle config schemas/metaschema_public/tables/composite_type/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table schemas/metaschema_public/types/object_category] 2026-05-29T00:00:00Z devin # add metaschema_public.composite_type table for generated composite types +schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet [schemas/metaschema_public/tables/schema/table] 2026-06-18T00:00:03Z devin # enforce one-way ratchet on api_exposure (never_expose is permanent) diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql new file mode 100644 index 00000000..4efcd17f --- /dev/null +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql @@ -0,0 +1,8 @@ +-- Revert schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet from pg + +BEGIN; + +DROP TRIGGER IF EXISTS _000003_enforce_api_exposure_ratchet ON metaschema_public.schema; +DROP FUNCTION IF EXISTS metaschema_public.tg_enforce_api_exposure_ratchet(); + +COMMIT; diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/types/api_exposure_level.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/types/api_exposure_level.sql new file mode 100644 index 00000000..0dca9b86 --- /dev/null +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/types/api_exposure_level.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/types/api_exposure_level from pg + +BEGIN; + +DROP TYPE metaschema_public.api_exposure_level; + +COMMIT; diff --git a/packages/metaschema-schema/sql/metaschema-schema--0.26.5.sql b/packages/metaschema-schema/sql/metaschema-schema--0.29.0.sql similarity index 96% rename from packages/metaschema-schema/sql/metaschema-schema--0.26.5.sql rename to packages/metaschema-schema/sql/metaschema-schema--0.29.0.sql index 82a14e02..486bf55f 100644 --- a/packages/metaschema-schema/sql/metaschema-schema--0.26.5.sql +++ b/packages/metaschema-schema/sql/metaschema-schema--0.29.0.sql @@ -25,7 +25,9 @@ ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public ALTER DEFAULT PRIVILEGES IN SCHEMA metaschema_public GRANT ALL ON FUNCTIONS TO authenticated; -CREATE TYPE metaschema_public.object_category AS ENUM ('core', 'module', 'app'); +CREATE TYPE metaschema_public.object_category AS ENUM ('core', 'module', 'permissions', 'auth', 'memberships', 'app'); + +CREATE TYPE metaschema_public.api_exposure_level AS ENUM ('exposable', 'internal_only', 'never_expose'); CREATE TABLE metaschema_public.database ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -57,6 +59,7 @@ CREATE TABLE metaschema_public.schema ( scope int NULL, tags citext[] NOT NULL DEFAULT '{}', is_public boolean NOT NULL DEFAULT true, + api_exposure metaschema_public.api_exposure_level NOT NULL DEFAULT 'exposable', created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now(), CONSTRAINT db_fkey @@ -526,9 +529,14 @@ CREATE INDEX view_table_id_idx ON metaschema_public.view (table_id); CREATE TABLE metaschema_public.view_table ( id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL DEFAULT uuid_nil(), view_id uuid NOT NULL, table_id uuid NOT NULL, join_order int NOT NULL DEFAULT 0, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, CONSTRAINT view_fkey FOREIGN KEY(view_id) REFERENCES metaschema_public.view (id) @@ -542,6 +550,8 @@ CREATE TABLE metaschema_public.view_table ( COMMENT ON TABLE metaschema_public.view_table IS 'Junction table linking views to their joined tables for referential integrity'; +CREATE INDEX view_table_database_id_idx ON metaschema_public.view_table (database_id); + CREATE INDEX view_table_view_id_idx ON metaschema_public.view_table (view_id); CREATE INDEX view_table_table_id_idx ON metaschema_public.view_table (table_id); @@ -905,7 +915,7 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzFilePath', 'authz_file_path', 'authz', 'File Path Share', 'Path-scoped file sharing via ltree containment. Grants access when a path_shares row matches the current user, bucket, and an ancestor path with the required permission.', CAST('{"type":"object","properties":{"shares_schema":{"type":"string","description":"Schema of the path_shares table"},"shares_table":{"type":"string","description":"Name of the path_shares table"},"files_schema":{"type":"string","description":"Schema of the files table (used to qualify column references inside the EXISTS subquery)"},"files_table":{"type":"string","description":"Name of the files table (used to qualify column references inside the EXISTS subquery)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["shares_schema","shares_table","files_table","permission_field"]}' AS jsonb), CAST('{"storage","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + ('AuthzFilePath', 'authz_file_path', 'authz', 'File Path Share', 'Path-scoped file sharing via ltree containment. Grants access when a path_shares row matches the current user, bucket, and an ancestor path with the required permission.', CAST('{"type":"object","properties":{"shares_table_id":{"type":"string","format":"uuid","description":"UUID of the path_shares table (alternative to shares_schema/shares_table)"},"shares_schema":{"type":"string","description":"Schema of the path_shares table (or use shares_table_id)"},"shares_table":{"type":"string","description":"Name of the path_shares table (or use shares_table_id)"},"files_table_id":{"type":"string","format":"uuid","description":"UUID of the files table (alternative to files_schema/files_table)"},"files_schema":{"type":"string","description":"Schema of the files table (or use files_table_id)"},"files_table":{"type":"string","description":"Name of the files table (or use files_table_id)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["permission_field"]}' AS jsonb), CAST('{"storage","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, @@ -1041,7 +1051,7 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzRelatedMemberList', 'authz_related_member_list', 'authz', 'Related Member List', 'Array membership check in a related table.', '{"type":"object","properties":{"owned_schema":{"type":"string","description":"Schema of the related table"},"owned_table":{"type":"string","description":"Name of the related table"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_schema","owned_table","owned_table_key","owned_table_ref_key","this_object_key"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + ('AuthzRelatedMemberList', 'authz_related_member_list', 'authz', 'Related Member List', 'Array membership check in a related table.', CAST('{"type":"object","properties":{"owned_table_id":{"type":"string","format":"uuid","description":"UUID of the related table (alternative to owned_schema/owned_table)"},"owned_schema":{"type":"string","description":"Schema of the related table (or use owned_table_id)"},"owned_table":{"type":"string","description":"Name of the related table (or use owned_table_id)"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_table_key","owned_table_ref_key","this_object_key"]}' AS jsonb), CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, @@ -1576,6 +1586,23 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema = excluded.parameter_schema, tags = excluded.tags; +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('GuardStepUp', 'guard_step_up', 'guard', 'Guard Step-Up', 'Attaches a BEFORE trigger that calls require_step_up() to enforce recent password or MFA verification before allowing mutations. Requires a provisioned sessions_module (with app_settings_auth) for the target database. The step_up_window is read from app_settings_auth at runtime (default 30 minutes). Supports compound conditions (AND/OR/NOT), watch_fields (fire only when specific fields change), and simple condition_field/condition_value leaf conditions.', CAST('{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"step_up_type":{"type":"string","enum":["password","mfa","password_or_mfa"],"description":"Which verification method satisfies the step-up requirement","default":"password_or_mfa"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Which DML events require step-up verification","default":["UPDATE","DELETE"]},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":[]}' AS jsonb), CAST('{"guard","triggers","auth","step-up","mfa","security"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + INSERT INTO metaschema_public.node_type_registry ( name, slug, @@ -2179,4 +2206,23 @@ CREATE TABLE metaschema_public.composite_type ( CREATE INDEX composite_type_schema_id_idx ON metaschema_public.composite_type (schema_id); -CREATE INDEX composite_type_database_id_idx ON metaschema_public.composite_type (database_id); \ No newline at end of file +CREATE INDEX composite_type_database_id_idx ON metaschema_public.composite_type (database_id); + +CREATE FUNCTION metaschema_public.tg_enforce_api_exposure_ratchet() RETURNS trigger AS $EOFCODE$ +BEGIN + IF OLD.api_exposure = 'never_expose' THEN + RAISE EXCEPTION 'Cannot change api_exposure from ''never_expose'' on schema "%". This level is permanent and can only be removed via a direct database migration.', + OLD.name + USING ERRCODE = 'check_violation'; + END IF; + + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE TRIGGER _000003_enforce_api_exposure_ratchet + BEFORE UPDATE + ON metaschema_public.schema + FOR EACH ROW + WHEN (new.api_exposure IS DISTINCT FROM old.api_exposure) + EXECUTE PROCEDURE metaschema_public.tg_enforce_api_exposure_ratchet(); \ No newline at end of file diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql new file mode 100644 index 00000000..3799dc23 --- /dev/null +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet.sql @@ -0,0 +1,10 @@ +-- Verify schemas/metaschema_public/tables/schema/triggers/enforce_api_exposure_ratchet on pg + +BEGIN; + +SELECT has_function_privilege( + 'metaschema_public.tg_enforce_api_exposure_ratchet()', + 'execute' +); + +ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/view_table/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/view_table/table.sql index 7270a4bf..f9f7270c 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/view_table/table.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/view_table/table.sql @@ -2,7 +2,7 @@ BEGIN; -SELECT id, view_id, table_id, join_order +SELECT id, database_id, view_id, table_id, join_order FROM metaschema_public.view_table WHERE FALSE; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/types/api_exposure_level.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/types/api_exposure_level.sql new file mode 100644 index 00000000..86aa74bd --- /dev/null +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/types/api_exposure_level.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/types/api_exposure_level on pg + +BEGIN; + +SELECT 'exposable'::metaschema_public.api_exposure_level; +SELECT 'internal_only'::metaschema_public.api_exposure_level; +SELECT 'never_expose'::metaschema_public.api_exposure_level; + +ROLLBACK; From 62e34e8c154e9d18b61a54fbe631a7ef5526167b Mon Sep 17 00:00:00 2001 From: Gefei Date: Sun, 21 Jun 2026 23:57:57 -0700 Subject: [PATCH 2/2] Update snapshot --- .../__tests__/__snapshots__/modules.test.ts.snap | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap index 5b92fe11..3214058e 100644 --- a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap +++ b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap @@ -127,13 +127,13 @@ exports[`db_meta_modules should verify emails_module table structure 1`] = ` exports[`db_meta_modules should verify module table structures have database_id foreign keys 1`] = ` { - "constraintCount": 303415, + "constraintCount": 310682, } `; exports[`db_meta_modules should verify module tables have proper foreign key relationships 1`] = ` { - "constraintCount": 452638, + "constraintCount": 459905, "foreignTables": [ "database", "field",