Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion packages/metaschema-schema/Makefile
Original file line number Diff line number Diff line change
@@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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(),

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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,

Expand All @@ -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 );

Expand Down
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
Expand Up @@ -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;
2 changes: 1 addition & 1 deletion packages/metaschema-schema/metaschema-schema.control
Original file line number Diff line number Diff line change
@@ -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
Expand Down
2 changes: 1 addition & 1 deletion packages/metaschema-schema/package.json
Original file line number Diff line number Diff line change
@@ -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 <pyramation@gmail.com>",
"contributors": [
Expand Down
4 changes: 3 additions & 1 deletion packages/metaschema-schema/pgpm.plan
Original file line number Diff line number Diff line change
Expand Up @@ -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 <skitch@5b0c196eeb62> # add schemas/metaschema_private/schema
schemas/metaschema_public/schema 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/metaschema_public/schema
schemas/metaschema_public/types/object_category [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch <skitch@5b0c196eeb62> # add schemas/metaschema_public/types/object_category
schemas/metaschema_public/types/api_exposure_level [schemas/metaschema_public/schema] 2026-06-18T00:00:00Z devin <devin@cognition.ai> # 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 <skitch@5b0c196eeb62> # 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 <skitch@5b0c196eeb62> # 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 <skitch@5b0c196eeb62> # 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 <skitch@5b0c196eeb62> # 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 <skitch@5b0c196eeb62> # 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 <skitch@5b0c196eeb62> # add schemas/metaschema_public/tables/database/indexes/databases_database_unique_name_idx
Expand Down Expand Up @@ -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 <devin@cognition.ai> # 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 <developers@constructive.io> # 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 <devin@cognition.ai> # 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 <devin@cognition.ai> # enforce one-way ratchet on api_exposure (never_expose is permanent)
Original file line number Diff line number Diff line change
@@ -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;
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
-- Revert schemas/metaschema_public/types/api_exposure_level from pg

BEGIN;

DROP TYPE metaschema_public.api_exposure_level;

COMMIT;
Loading
Loading