From 58fa868584f16abb49ca59fde7fd763f71f2325a Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Feb 2026 11:12:42 +0000 Subject: [PATCH 1/3] feat: upstream COMMENT ON statements, inflection improvements, and other changes from constructive-db - Add 294 COMMENT ON statements across 29 files documenting tables, columns, domains, constraints, and procedures - Upstream inflection module improvements: uncountable words handling, Latin suffix overrides (schemata->schema, phenomena->phenomenon, etc.), octopi->octopuses fix, new test cases, should_skip_uncountable procedure - Add jwt-claims current_session_id procedure and 0.15.5 version - Add metaschema-modules relation_provision table (deploy/revert/verify) - Add metaschema-schema test snapshots - Add types 0.15.5 version - Sync metaschema-schema object_category type changes - Sync metaschema-modules field_module, table_module, rls_module changes - Various module version bumps and README updates --- .../tables/level_requirements/table.sql | 6 + .../status_public/tables/levels/table.sql | 1 + .../tables/user_achievements/table.sql | 5 + .../status_public/tables/user_steps/table.sql | 6 + .../sql/pgpm-achievements--0.15.3.sql | 18 +- .../app_jobs/tables/job_queues/table.sql | 7 + .../schemas/app_jobs/tables/jobs/table.sql | 16 + .../app_jobs/tables/scheduled_jobs/table.sql | 16 + .../sql/pgpm-database-jobs--0.15.3.sql | 38 +- .../tables/secrets_table/table.sql | 7 + .../pgpm-encrypted-secrets-table--0.15.3.sql | 9 +- packages/inflection/README.md | 2 +- .../inflection/__tests__/inflection.test.ts | 47 +- .../schemas/inflection/procedures/plural.sql | 5 + .../inflection/procedures/singular.sql | 5 + .../fixtures/1589249334312_fixture.sql | 33 +- packages/inflection/pgpm.plan | 6 +- .../sql/pgpm-inflection--0.15.3.sql | 43 +- .../app_jobs/tables/job_queues/table.sql | 7 + .../schemas/app_jobs/tables/jobs/table.sql | 15 + .../app_jobs/tables/scheduled_jobs/table.sql | 15 + packages/jobs/sql/pgpm-jobs--0.15.3.sql | 36 +- packages/jwt-claims/Makefile | 2 +- .../procedures/current_session_id.sql | 18 + packages/jwt-claims/pgpm-jwt-claims.control | 2 +- packages/jwt-claims/pgpm.plan | 1 + .../procedures/current_session_id.sql | 7 + .../sql/pgpm-jwt-claims--0.15.5.sql | 147 ++++++ .../procedures/current_session_id.sql | 7 + .../measurements/tables/quantities/table.sql | 8 + .../sql/pgpm-measurements--0.15.3.sql | 10 +- packages/metaschema-modules/README.md | 15 +- .../__tests__/modules.test.ts | 2 +- .../tables/field_module/table.sql | 7 +- .../tables/profiles_module/table.sql | 2 - .../tables/relation_provision/table.sql | 282 ++++++++++++ .../tables/rls_module/table.sql | 9 +- .../tables/table_module/table.sql | 17 +- .../tables/table_template_module/table.sql | 5 + packages/metaschema-modules/pgpm.plan | 3 +- .../tables/field_module/table.sql | 2 +- .../tables/relation_provision/table.sql | 7 + .../sql/metaschema-modules--0.15.5.sql | 426 ++++++++++++++---- .../tables/field_module/table.sql | 4 +- .../tables/profiles_module/table.sql | 2 +- .../tables/relation_provision/table.sql | 36 ++ .../tables/table_module/table.sql | 4 +- packages/metaschema-schema/README.md | 16 +- .../__tests__/__snapshots__/meta.test.ts.snap | 147 ++++++ .../metaschema-schema/__tests__/meta.test.ts | 195 +++++++- .../tables/database/table.sql | 8 +- .../metaschema_public/tables/field/table.sql | 22 +- .../tables/foreign_key_constraint/table.sql | 2 +- .../metaschema_public/tables/index/table.sql | 1 + .../metaschema_public/tables/policy/table.sql | 4 + .../tables/procedure/table.sql | 3 + .../metaschema_public/tables/schema/table.sql | 3 + .../metaschema_public/tables/table/table.sql | 2 - .../tables/trigger/table.sql | 4 +- .../tables/unique_constraint/table.sql | 3 + .../types/object_category.sql | 4 + packages/metaschema-schema/pgpm.plan | 21 +- .../types/object_category.sql | 2 +- .../sql/metaschema-schema--0.15.5.sql | 96 ++-- .../types/object_category.sql | 2 + .../tables/api_modules/table.sql | 7 + .../tables/api_schemas/table.sql | 6 + .../services_public/tables/apis/table.sql | 9 + .../services_public/tables/apps/table.sql | 11 + .../services_public/tables/domains/table.sql | 8 + .../tables/site_metadata/table.sql | 8 + .../tables/site_modules/table.sql | 7 + .../tables/site_themes/table.sql | 6 + .../services_public/tables/sites/table.sql | 11 + packages/types/Makefile | 2 +- .../types/__tests__/domains.pgutils.test.ts | 317 ++++++------- packages/types/__tests__/domains.test.ts | 367 ++++++--------- packages/types/pgpm-types.control | 2 +- packages/types/sql/pgpm-types--0.15.5.sql | 71 +++ 79 files changed, 2110 insertions(+), 625 deletions(-) create mode 100644 packages/jwt-claims/deploy/schemas/jwt_private/procedures/current_session_id.sql create mode 100644 packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql create mode 100644 packages/jwt-claims/sql/pgpm-jwt-claims--0.15.5.sql create mode 100644 packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql create mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql create mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql create mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql create mode 100644 packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap create mode 100644 packages/types/sql/pgpm-types--0.15.5.sql diff --git a/packages/achievements/deploy/schemas/status_public/tables/level_requirements/table.sql b/packages/achievements/deploy/schemas/status_public/tables/level_requirements/table.sql index 95c80f822..b762b7036 100644 --- a/packages/achievements/deploy/schemas/status_public/tables/level_requirements/table.sql +++ b/packages/achievements/deploy/schemas/status_public/tables/level_requirements/table.sql @@ -15,6 +15,12 @@ CREATE TABLE status_public.level_requirements ( ); COMMENT ON TABLE status_public.level_requirements IS 'Requirements to achieve a level'; +COMMENT ON COLUMN status_public.level_requirements.id IS 'Unique identifier for this requirement'; +COMMENT ON COLUMN status_public.level_requirements.name IS 'Requirement name (e.g. posts_created, logins); matches user_steps.name'; +COMMENT ON COLUMN status_public.level_requirements.level IS 'Level this requirement belongs to (references levels.name)'; +COMMENT ON COLUMN status_public.level_requirements.required_count IS 'Number of steps needed to satisfy this requirement (default 1)'; +COMMENT ON COLUMN status_public.level_requirements.priority IS 'Display/evaluation order; lower numbers are checked first (default 100)'; + CREATE INDEX ON status_public.level_requirements (name, level, priority); GRANT SELECT ON TABLE status_public.levels TO authenticated; diff --git a/packages/achievements/deploy/schemas/status_public/tables/levels/table.sql b/packages/achievements/deploy/schemas/status_public/tables/levels/table.sql index e39c966d8..077ba3ae6 100644 --- a/packages/achievements/deploy/schemas/status_public/tables/levels/table.sql +++ b/packages/achievements/deploy/schemas/status_public/tables/levels/table.sql @@ -9,6 +9,7 @@ CREATE TABLE status_public.levels ( ); COMMENT ON TABLE status_public.levels IS 'Levels for achievement'; +COMMENT ON COLUMN status_public.levels.name IS 'Unique level name used as the primary key (e.g. bronze, silver, gold)'; GRANT SELECT ON TABLE status_public.levels TO public; diff --git a/packages/achievements/deploy/schemas/status_public/tables/user_achievements/table.sql b/packages/achievements/deploy/schemas/status_public/tables/user_achievements/table.sql index 5796121a4..0f9e9756d 100644 --- a/packages/achievements/deploy/schemas/status_public/tables/user_achievements/table.sql +++ b/packages/achievements/deploy/schemas/status_public/tables/user_achievements/table.sql @@ -14,6 +14,11 @@ CREATE TABLE status_public.user_achievements ( ); COMMENT ON TABLE status_public.user_achievements IS 'This table represents the users progress for particular level requirements, tallying the total count. This table is updated via triggers and should not be updated maually.'; +COMMENT ON COLUMN status_public.user_achievements.id IS 'Unique identifier for this achievement progress record'; +COMMENT ON COLUMN status_public.user_achievements.user_id IS 'User whose progress is being tracked'; +COMMENT ON COLUMN status_public.user_achievements.name IS 'Name of the level requirement this progress relates to'; +COMMENT ON COLUMN status_public.user_achievements.count IS 'Accumulated count toward the requirement (updated by triggers)'; +COMMENT ON COLUMN status_public.user_achievements.created_at IS 'Timestamp when this progress record was first created'; CREATE INDEX ON status_public.user_achievements (user_id, name); diff --git a/packages/achievements/deploy/schemas/status_public/tables/user_steps/table.sql b/packages/achievements/deploy/schemas/status_public/tables/user_steps/table.sql index 2c1791f9b..3164a30c8 100644 --- a/packages/achievements/deploy/schemas/status_public/tables/user_steps/table.sql +++ b/packages/achievements/deploy/schemas/status_public/tables/user_steps/table.sql @@ -13,6 +13,12 @@ CREATE TABLE status_public.user_steps ( ); COMMENT ON TABLE status_public.user_steps IS 'The user achieving a requirement for a level. Log table that has every single step ever taken.'; +COMMENT ON COLUMN status_public.user_steps.id IS 'Unique identifier for this step record'; +COMMENT ON COLUMN status_public.user_steps.user_id IS 'User who performed this step'; +COMMENT ON COLUMN status_public.user_steps.name IS 'Name of the level requirement this step counts toward'; +COMMENT ON COLUMN status_public.user_steps.count IS 'Number of units this step contributes (default 1)'; +COMMENT ON COLUMN status_public.user_steps.created_at IS 'Timestamp when this step was recorded'; + CREATE INDEX ON status_public.user_steps (user_id, name); COMMIT; diff --git a/packages/achievements/sql/pgpm-achievements--0.15.3.sql b/packages/achievements/sql/pgpm-achievements--0.15.3.sql index acd543a88..07b94c52a 100644 --- a/packages/achievements/sql/pgpm-achievements--0.15.3.sql +++ b/packages/achievements/sql/pgpm-achievements--0.15.3.sql @@ -22,6 +22,11 @@ CREATE TABLE status_public.user_steps ( ); COMMENT ON TABLE status_public.user_steps IS 'The user achieving a requirement for a level. Log table that has every single step ever taken.'; +COMMENT ON COLUMN status_public.user_steps.id IS 'Unique identifier for this step record'; +COMMENT ON COLUMN status_public.user_steps.user_id IS 'User who performed this step'; +COMMENT ON COLUMN status_public.user_steps.name IS 'Name of the level requirement this step counts toward'; +COMMENT ON COLUMN status_public.user_steps.count IS 'Number of units this step contributes (default 1)'; +COMMENT ON COLUMN status_public.user_steps.created_at IS 'Timestamp when this step was recorded'; CREATE INDEX ON status_public.user_steps (user_id, name); @@ -124,6 +129,11 @@ CREATE TABLE status_public.user_achievements ( ); COMMENT ON TABLE status_public.user_achievements IS 'This table represents the users progress for particular level requirements, tallying the total count. This table is updated via triggers and should not be updated maually.'; +COMMENT ON COLUMN status_public.user_achievements.id IS 'Unique identifier for this achievement progress record'; +COMMENT ON COLUMN status_public.user_achievements.user_id IS 'User whose progress is being tracked'; +COMMENT ON COLUMN status_public.user_achievements.name IS 'Name of the level requirement this progress relates to'; +COMMENT ON COLUMN status_public.user_achievements.count IS 'Accumulated count toward the requirement (updated by triggers)'; +COMMENT ON COLUMN status_public.user_achievements.created_at IS 'Timestamp when this progress record was first created'; CREATE INDEX ON status_public.user_achievements (user_id, name); @@ -145,6 +155,7 @@ CREATE TABLE status_public.levels ( ); COMMENT ON TABLE status_public.levels IS 'Levels for achievement'; +COMMENT ON COLUMN status_public.levels.name IS 'Unique level name used as the primary key (e.g. bronze, silver, gold)'; GRANT SELECT ON status_public.levels TO PUBLIC; @@ -158,6 +169,11 @@ CREATE TABLE status_public.level_requirements ( ); COMMENT ON TABLE status_public.level_requirements IS 'Requirements to achieve a level'; +COMMENT ON COLUMN status_public.level_requirements.id IS 'Unique identifier for this requirement'; +COMMENT ON COLUMN status_public.level_requirements.name IS 'Requirement name (e.g. posts_created, logins); matches user_steps.name'; +COMMENT ON COLUMN status_public.level_requirements.level IS 'Level this requirement belongs to (references levels.name)'; +COMMENT ON COLUMN status_public.level_requirements.required_count IS 'Number of steps needed to satisfy this requirement (default 1)'; +COMMENT ON COLUMN status_public.level_requirements.priority IS 'Display/evaluation order; lower numbers are checked first (default 100)'; CREATE INDEX ON status_public.level_requirements (name, level, priority); @@ -261,4 +277,4 @@ CREATE TRIGGER update_achievements_tg AFTER INSERT ON status_public.user_steps FOR EACH ROW - EXECUTE PROCEDURE status_private.tg_update_achievements_tg(); \ No newline at end of file + EXECUTE PROCEDURE status_private.tg_update_achievements_tg(); diff --git a/packages/database-jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql b/packages/database-jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql index 4ad1305c9..dd003c348 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql @@ -8,5 +8,12 @@ CREATE TABLE app_jobs.job_queues ( locked_at timestamptz, locked_by text ); + +COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue'; +COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue'; +COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue'; +COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing'; +COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock'; + COMMIT; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql index b5e27f5da..48ea7c146 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/tables/jobs/table.sql @@ -23,5 +23,21 @@ CREATE TABLE app_jobs.jobs ( CHECK (length(locked_by) > 3), UNIQUE (key) ); + +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue with database scoping: each row is a pending or in-progress task for a specific database'; +COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; +COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to, for multi-tenant job isolation'; +COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; +COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; +COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; +COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)'; +COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution'; +COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far'; +COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed'; +COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key'; +COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; +COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; +COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; + COMMIT; diff --git a/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql index 2f1506fb9..75d81c9e3 100644 --- a/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql +++ b/packages/database-jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -23,5 +23,21 @@ CREATE TABLE app_jobs.scheduled_jobs ( CHECK (length(locked_by) > 3), UNIQUE (key) ); + +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions with database scoping: each row spawns jobs on a schedule for a specific database'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to, for multi-tenant isolation'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule'; + COMMIT; diff --git a/packages/database-jobs/sql/pgpm-database-jobs--0.15.3.sql b/packages/database-jobs/sql/pgpm-database-jobs--0.15.3.sql index f8dff738b..73c98278a 100644 --- a/packages/database-jobs/sql/pgpm-database-jobs--0.15.3.sql +++ b/packages/database-jobs/sql/pgpm-database-jobs--0.15.3.sql @@ -134,6 +134,21 @@ CREATE TABLE app_jobs.scheduled_jobs ( UNIQUE (key) ); +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions with database scoping: each row spawns jobs on a schedule for a specific database'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.database_id IS 'Database this scheduled job belongs to, for multi-tenant isolation'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule'; + CREATE FUNCTION app_jobs.do_notify() RETURNS trigger AS $EOFCODE$ BEGIN PERFORM @@ -176,6 +191,21 @@ CREATE TABLE app_jobs.jobs ( UNIQUE (key) ); +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue with database scoping: each row is a pending or in-progress task for a specific database'; +COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; +COMMENT ON COLUMN app_jobs.jobs.database_id IS 'Database this job belongs to, for multi-tenant job isolation'; +COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; +COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; +COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; +COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)'; +COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution'; +COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far'; +COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed'; +COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key'; +COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; +COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; +COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; + ALTER TABLE app_jobs.jobs ADD COLUMN created_at timestamptz; @@ -275,6 +305,12 @@ CREATE TABLE app_jobs.job_queues ( locked_by text ); +COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue'; +COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue'; +COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue'; +COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing'; +COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock'; + CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by); GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.job_queues TO administrator; @@ -766,4 +802,4 @@ BEGIN RETURN v_job; END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; \ No newline at end of file +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; diff --git a/packages/encrypted-secrets-table/deploy/schemas/secrets_schema/tables/secrets_table/table.sql b/packages/encrypted-secrets-table/deploy/schemas/secrets_schema/tables/secrets_table/table.sql index 50f6a9356..3466fdc0c 100644 --- a/packages/encrypted-secrets-table/deploy/schemas/secrets_schema/tables/secrets_table/table.sql +++ b/packages/encrypted-secrets-table/deploy/schemas/secrets_schema/tables/secrets_table/table.sql @@ -13,4 +13,11 @@ CREATE TABLE secrets_schema.secrets_table ( UNIQUE(secrets_owned_field, name) ); +COMMENT ON TABLE secrets_schema.secrets_table IS 'Encrypted key-value secret storage: stores secrets as either raw bytea or encrypted text, scoped to an owning entity'; +COMMENT ON COLUMN secrets_schema.secrets_table.id IS 'Unique identifier for this secret'; +COMMENT ON COLUMN secrets_schema.secrets_table.secrets_owned_field IS 'UUID of the owning entity (e.g. user, organization); combined with name forms a unique key'; +COMMENT ON COLUMN secrets_schema.secrets_table.name IS 'Name/key for this secret within its owner scope'; +COMMENT ON COLUMN secrets_schema.secrets_table.secrets_value_field IS 'Raw binary secret value (mutually exclusive with secrets_enc_field)'; +COMMENT ON COLUMN secrets_schema.secrets_table.secrets_enc_field IS 'Encrypted text secret value (mutually exclusive with secrets_value_field)'; + COMMIT; diff --git a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.3.sql b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.3.sql index a568c8c86..a9c317cfd 100644 --- a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.3.sql +++ b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.3.sql @@ -10,6 +10,13 @@ CREATE TABLE secrets_schema.secrets_table ( UNIQUE (secrets_owned_field, name) ); +COMMENT ON TABLE secrets_schema.secrets_table IS 'Encrypted key-value secret storage: stores secrets as either raw bytea or encrypted text, scoped to an owning entity'; +COMMENT ON COLUMN secrets_schema.secrets_table.id IS 'Unique identifier for this secret'; +COMMENT ON COLUMN secrets_schema.secrets_table.secrets_owned_field IS 'UUID of the owning entity (e.g. user, organization); combined with name forms a unique key'; +COMMENT ON COLUMN secrets_schema.secrets_table.name IS 'Name/key for this secret within its owner scope'; +COMMENT ON COLUMN secrets_schema.secrets_table.secrets_value_field IS 'Raw binary secret value (mutually exclusive with secrets_enc_field)'; +COMMENT ON COLUMN secrets_schema.secrets_table.secrets_enc_field IS 'Encrypted text secret value (mutually exclusive with secrets_value_field)'; + CREATE FUNCTION secrets_schema.tg_hash_secrets() RETURNS trigger AS $EOFCODE$ BEGIN IF (NEW.secrets_enc_field = 'crypt') THEN @@ -34,4 +41,4 @@ CREATE TRIGGER hash_secrets_insert BEFORE INSERT ON secrets_schema.secrets_table FOR EACH ROW - EXECUTE PROCEDURE secrets_schema.tg_hash_secrets(); \ No newline at end of file + EXECUTE PROCEDURE secrets_schema.tg_hash_secrets(); diff --git a/packages/inflection/README.md b/packages/inflection/README.md index f7ffb998c..f9dbfdf2f 100644 --- a/packages/inflection/README.md +++ b/packages/inflection/README.md @@ -297,7 +297,7 @@ SELECT slug FROM blog_posts; ## Integration Examples -### With @pgpm/metaschema-schema +### With @pgpm/db-meta-schema Use inflection for schema introspection and code generation: diff --git a/packages/inflection/__tests__/inflection.test.ts b/packages/inflection/__tests__/inflection.test.ts index 04efb53e4..d1bb15c9b 100644 --- a/packages/inflection/__tests__/inflection.test.ts +++ b/packages/inflection/__tests__/inflection.test.ts @@ -154,7 +154,26 @@ describe('inflection', () => { { name: 'children', result: 'children' }, { name: 'child', result: 'children' }, { name: 'man', result: 'men' }, - { name: 'men', result: 'men' } + { name: 'men', result: 'men' }, + // node.inflection v3 sync: octopus/virus use -uses + { name: 'octopus', result: 'octopuses' }, + { name: 'virus', result: 'viruses' }, + { name: 'octopuses', result: 'octopuses' }, + { name: 'viruses', result: 'viruses' }, + // node.inflection v3 sync: drive, focus, bonus, database + { name: 'drive', result: 'drives' }, + { name: 'drives', result: 'drives' }, + { name: 'focus', result: 'focuses' }, + { name: 'bonus', result: 'bonuses' }, + { name: 'database', result: 'databases' }, + { name: 'databases', result: 'databases' }, + // uncountable words + { name: 'sheep', result: 'sheep' }, + { name: 'equipment', result: 'equipment' }, + { name: 'information', result: 'information' }, + { name: 'deer', result: 'deer' }, + { name: 'series', result: 'series' }, + { name: 'species', result: 'species' } ] ); @@ -175,7 +194,31 @@ describe('inflection', () => { { name: 'children', result: 'child' }, { name: 'child', result: 'child' }, { name: 'man', result: 'man' }, - { name: 'men', result: 'man' } + { name: 'men', result: 'man' }, + // node.inflection v3 sync: octopus/virus use -uses + { name: 'octopuses', result: 'octopus' }, + { name: 'viruses', result: 'virus' }, + { name: 'octopus', result: 'octopus' }, + { name: 'virus', result: 'virus' }, + // node.inflection v3 sync: drive, database + { name: 'drives', result: 'drive' }, + { name: 'drive', result: 'drive' }, + { name: 'databases', result: 'database' }, + { name: 'database', result: 'database' }, + { name: 'bonuses', result: 'bonus' }, + // Latin suffix overrides (PostGraphile-compatible) + { name: 'schemata', result: 'schema' }, + { name: 'phenomena', result: 'phenomenon' }, + { name: 'memoranda', result: 'memorandum' }, + { name: 'curricula', result: 'curriculum' }, + { name: 'criteria', result: 'criterion' }, + { name: 'media', result: 'medium' }, + { name: 'data', result: 'datum' }, + { name: 'strata', result: 'stratum' }, + // uncountable words + { name: 'sheep', result: 'sheep' }, + { name: 'equipment', result: 'equipment' }, + { name: 'information', result: 'information' } ] ); }); diff --git a/packages/inflection/deploy/schemas/inflection/procedures/plural.sql b/packages/inflection/deploy/schemas/inflection/procedures/plural.sql index 582d4bfd6..83f812eb1 100644 --- a/packages/inflection/deploy/schemas/inflection/procedures/plural.sql +++ b/packages/inflection/deploy/schemas/inflection/procedures/plural.sql @@ -2,6 +2,7 @@ -- requires: schemas/inflection/schema -- requires: schemas/inflection/tables/inflection_rules/table +-- requires: schemas/inflection/procedures/should_skip_uncountable BEGIN; @@ -12,6 +13,10 @@ DECLARE result record; matches text[]; BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + FOR result IN SELECT * FROM inflection.inflection_rules where type='plural' LOOP diff --git a/packages/inflection/deploy/schemas/inflection/procedures/singular.sql b/packages/inflection/deploy/schemas/inflection/procedures/singular.sql index 56c860009..e9ee3a0f7 100644 --- a/packages/inflection/deploy/schemas/inflection/procedures/singular.sql +++ b/packages/inflection/deploy/schemas/inflection/procedures/singular.sql @@ -2,6 +2,7 @@ -- requires: schemas/inflection/schema -- requires: schemas/inflection/tables/inflection_rules/table +-- requires: schemas/inflection/procedures/should_skip_uncountable BEGIN; @@ -12,6 +13,10 @@ DECLARE result record; matches text[]; BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + FOR result IN SELECT * FROM inflection.inflection_rules where type='singular' LOOP diff --git a/packages/inflection/deploy/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql b/packages/inflection/deploy/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql index f9f54820b..7f4ab4eda 100644 --- a/packages/inflection/deploy/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql +++ b/packages/inflection/deploy/schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture.sql @@ -7,11 +7,14 @@ BEGIN; INSERT INTO inflection.inflection_rules (type, test, replacement) VALUES + -- plural guards: already-plural words return as-is (NULL replacement) ('plural', '^(m|wom)en$', NULL), ('plural', '(pe)ople$', NULL), ('plural', '(child)ren$', NULL), ('plural', '([ti])a$', NULL), ('plural', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', NULL), + ('plural', '(database)s$', NULL), + ('plural', '(drive)s$', NULL), ('plural', '(hi|ti)ves$', NULL), ('plural', '(curve)s$', NULL), ('plural', '([lr])ves$', NULL), @@ -25,11 +28,12 @@ INSERT INTO inflection.inflection_rules ('plural', '(o)es$', NULL), ('plural', '(shoe)s$', NULL), ('plural', '(cris|ax|test)es$', NULL), - ('plural', '(octop|vir)i$', NULL), + ('plural', '(octop|vir)uses$', NULL), ('plural', '(alias|canvas|status|campus)es$', NULL), - ('plural', '^(summons)es$', NULL), + ('plural', '^(summons|bonus)es$', NULL), ('plural', '^(ox)en', NULL), ('plural', '(matr)ices$', NULL), + ('plural', '(vert|ind)ices$', NULL), ('plural', '^feet$', NULL), ('plural', '^teeth$', NULL), ('plural', '^geese$', NULL), @@ -37,19 +41,22 @@ INSERT INTO inflection.inflection_rules ('plural', '^(whereas)es$', NULL), ('plural', '^(criteri)a$', NULL), ('plural', '^genera$', NULL), + -- plural replacement rules ('plural', '^(m|wom)an$', E'\\1en'), ('plural', '(pe)rson$', E'\\1ople'), ('plural', '(child)$', E'\\1ren'), + ('plural', '(drive)$', E'\\1s'), ('plural', '^(ox)$', E'\\1en'), ('plural', '(ax|test)is$', E'\\1es'), - ('plural', '(octop|vir)us$', E'\\1i'), + ('plural', '(octop|vir)us$', E'\\1uses'), ('plural', '(alias|status|canvas|campus)$', E'\\1es'), - ('plural', '^(summons)$', E'\\1es'), + ('plural', '^(summons|bonus)$', E'\\1es'), ('plural', '(bu)s$', E'\\1ses'), ('plural', '(buffal|tomat|potat)o$', E'\\1oes'), ('plural', '([ti])um$', E'\\1a'), ('plural', 'sis$', E'ses'), ('plural', '(?:([^f])fe|([lr])f)$', E'\\1\\2ves'), + ('plural', '^(focus)$', E'\\1es'), ('plural', '(hi|ti)ve$', E'\\1ves'), ('plural', '([^aeiouy]|qu)y$', E'\\1ies'), ('plural', '(matr)ix$', E'\\1ices'), @@ -65,23 +72,27 @@ INSERT INTO inflection.inflection_rules ('plural', '^genus$', E'genera'), ('plural', 's$', E's'), ('plural', '$', E's'), + -- singular guards: already-singular words return as-is (NULL replacement) ('singular', '^(m|wom)an$', NULL), ('singular', '(pe)rson$', NULL), ('singular', '(child)$', NULL), + ('singular', '(drive)$', NULL), ('singular', '^(ox)$', NULL), ('singular', '(ax|test)is$', NULL), ('singular', '(octop|vir)us$', NULL), ('singular', '(alias|status|canvas|campus)$', NULL), - ('singular', '^(summons)$', NULL), + ('singular', '^(summons|bonus)$', NULL), ('singular', '(bu)s$', NULL), ('singular', '(buffal|tomat|potat)o$', NULL), ('singular', '([ti])um$', NULL), ('singular', 'sis$', NULL), ('singular', '(?:([^f])fe|([lr])f)$', NULL), + ('singular', '^(focus)$', NULL), ('singular', '(hi|ti)ve$', NULL), ('singular', '([^aeiouy]|qu)y$', NULL), ('singular', '(x|ch|ss|sh)$', NULL), ('singular', '(matr)ix$', NULL), + ('singular', '(vert|ind)ex$', NULL), ('singular', '([m|l])ouse$', NULL), ('singular', '^foot$', NULL), ('singular', '^tooth$', NULL), @@ -90,11 +101,19 @@ INSERT INTO inflection.inflection_rules ('singular', '^(whereas)$', NULL), ('singular', '^(criteri)on$', NULL), ('singular', '^genus$', NULL), + -- singular replacement rules ('singular', '^(m|wom)en$', E'\\1an'), ('singular', '(pe)ople$', E'\\1rson'), ('singular', '(child)ren$', E'\\1'), + ('singular', '(database)s$', E'\\1'), + ('singular', '(drive)s$', E'\\1'), ('singular', '^genera$', E'genus'), ('singular', '^(criteri)a$', E'\\1on'), + -- Latin suffix overrides (PostGraphile-compatible) + ('singular', '(schema)ta$', E'\\1'), + ('singular', '(phenomen)a$', E'\\1on'), + ('singular', '(memorand)a$', E'\\1um'), + ('singular', '(curricul)a$', E'\\1um'), ('singular', '([ti])a$', E'\\1um'), ('singular', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', E'\\1\\2sis'), ('singular', '(hi|ti)ves$', E'\\1ve'), @@ -111,9 +130,9 @@ INSERT INTO inflection.inflection_rules ('singular', '(o)es$', E'\\1'), ('singular', '(shoe)s$', E'\\1'), ('singular', '(cris|ax|test)es$', E'\\1is'), - ('singular', '(octop|vir)i$', E'\\1us'), + ('singular', '(octop|vir)uses$', E'\\1us'), ('singular', '(alias|canvas|status|campus)es$', E'\\1'), - ('singular', '^(summons)es$', E'\\1'), + ('singular', '^(summons|bonus)es$', E'\\1'), ('singular', '^(ox)en', E'\\1'), ('singular', '(matr)ices$', E'\\1ix'), ('singular', '(vert|ind)ices$', E'\\1ex'), diff --git a/packages/inflection/pgpm.plan b/packages/inflection/pgpm.plan index 46a8404bb..d289d21f8 100644 --- a/packages/inflection/pgpm.plan +++ b/packages/inflection/pgpm.plan @@ -11,10 +11,10 @@ schemas/inflection/procedures/camel [schemas/inflection/schema schemas/inflectio schemas/inflection/procedures/dashed [schemas/inflection/schema schemas/inflection/procedures/underscore] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/dashed schemas/inflection/procedures/pascal [schemas/inflection/schema schemas/inflection/procedures/camel] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/pascal schemas/inflection/tables/inflection_rules/table [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/tables/inflection_rules/table -schemas/inflection/procedures/plural [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/plural schemas/inflection/procedures/uncountable_words [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/uncountable_words schemas/inflection/procedures/should_skip_uncountable [schemas/inflection/schema schemas/inflection/procedures/uncountable_words] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/should_skip_uncountable -schemas/inflection/procedures/singular [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/singular +schemas/inflection/procedures/plural [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table schemas/inflection/procedures/should_skip_uncountable] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/plural +schemas/inflection/procedures/singular [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table schemas/inflection/procedures/should_skip_uncountable] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/singular schemas/inflection/procedures/slugify [schemas/inflection/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection/procedures/slugify schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table] 2017-08-11T08:11:51Z skitch # add schemas/inflection/tables/inflection_rules/fixtures/1589249334312_fixture -schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table] 2017-08-11T08:11:51Z skitch # add schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx \ No newline at end of file +schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx [schemas/inflection/schema schemas/inflection/tables/inflection_rules/table] 2017-08-11T08:11:51Z skitch # add schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx diff --git a/packages/inflection/sql/pgpm-inflection--0.15.3.sql b/packages/inflection/sql/pgpm-inflection--0.15.3.sql index 14e7bdb88..fef389ef4 100644 --- a/packages/inflection/sql/pgpm-inflection--0.15.3.sql +++ b/packages/inflection/sql/pgpm-inflection--0.15.3.sql @@ -242,6 +242,10 @@ DECLARE result record; matches text[]; BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + FOR result IN SELECT * FROM inflection.inflection_rules where type='plural' LOOP @@ -272,6 +276,10 @@ DECLARE result record; matches text[]; BEGIN + IF inflection.should_skip_uncountable(lower(str)) THEN + return str; + END IF; + FOR result IN SELECT * FROM inflection.inflection_rules where type='singular' LOOP @@ -334,11 +342,14 @@ INSERT INTO inflection.inflection_rules ( test, replacement ) VALUES + -- plural guards: already-plural words return as-is (NULL replacement) ('plural', '^(m|wom)en$', NULL), ('plural', '(pe)ople$', NULL), ('plural', '(child)ren$', NULL), ('plural', '([ti])a$', NULL), ('plural', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', NULL), + ('plural', '(database)s$', NULL), + ('plural', '(drive)s$', NULL), ('plural', '(hi|ti)ves$', NULL), ('plural', '(curve)s$', NULL), ('plural', '([lr])ves$', NULL), @@ -352,11 +363,12 @@ INSERT INTO inflection.inflection_rules ( ('plural', '(o)es$', NULL), ('plural', '(shoe)s$', NULL), ('plural', '(cris|ax|test)es$', NULL), - ('plural', '(octop|vir)i$', NULL), + ('plural', '(octop|vir)uses$', NULL), ('plural', '(alias|canvas|status|campus)es$', NULL), - ('plural', '^(summons)es$', NULL), + ('plural', '^(summons|bonus)es$', NULL), ('plural', '^(ox)en', NULL), ('plural', '(matr)ices$', NULL), + ('plural', '(vert|ind)ices$', NULL), ('plural', '^feet$', NULL), ('plural', '^teeth$', NULL), ('plural', '^geese$', NULL), @@ -364,19 +376,22 @@ INSERT INTO inflection.inflection_rules ( ('plural', '^(whereas)es$', NULL), ('plural', '^(criteri)a$', NULL), ('plural', '^genera$', NULL), + -- plural replacement rules ('plural', '^(m|wom)an$', E'\\1en'), ('plural', '(pe)rson$', E'\\1ople'), ('plural', '(child)$', E'\\1ren'), + ('plural', '(drive)$', E'\\1s'), ('plural', '^(ox)$', E'\\1en'), ('plural', '(ax|test)is$', E'\\1es'), - ('plural', '(octop|vir)us$', E'\\1i'), + ('plural', '(octop|vir)us$', E'\\1uses'), ('plural', '(alias|status|canvas|campus)$', E'\\1es'), - ('plural', '^(summons)$', E'\\1es'), + ('plural', '^(summons|bonus)$', E'\\1es'), ('plural', '(bu)s$', E'\\1ses'), ('plural', '(buffal|tomat|potat)o$', E'\\1oes'), ('plural', '([ti])um$', E'\\1a'), ('plural', 'sis$', 'ses'), ('plural', '(?:([^f])fe|([lr])f)$', E'\\1\\2ves'), + ('plural', '^(focus)$', E'\\1es'), ('plural', '(hi|ti)ve$', E'\\1ves'), ('plural', '([^aeiouy]|qu)y$', E'\\1ies'), ('plural', '(matr)ix$', E'\\1ices'), @@ -392,23 +407,27 @@ INSERT INTO inflection.inflection_rules ( ('plural', '^genus$', 'genera'), ('plural', 's$', 's'), ('plural', '$', 's'), + -- singular guards: already-singular words return as-is (NULL replacement) ('singular', '^(m|wom)an$', NULL), ('singular', '(pe)rson$', NULL), ('singular', '(child)$', NULL), + ('singular', '(drive)$', NULL), ('singular', '^(ox)$', NULL), ('singular', '(ax|test)is$', NULL), ('singular', '(octop|vir)us$', NULL), ('singular', '(alias|status|canvas|campus)$', NULL), - ('singular', '^(summons)$', NULL), + ('singular', '^(summons|bonus)$', NULL), ('singular', '(bu)s$', NULL), ('singular', '(buffal|tomat|potat)o$', NULL), ('singular', '([ti])um$', NULL), ('singular', 'sis$', NULL), ('singular', '(?:([^f])fe|([lr])f)$', NULL), + ('singular', '^(focus)$', NULL), ('singular', '(hi|ti)ve$', NULL), ('singular', '([^aeiouy]|qu)y$', NULL), ('singular', '(x|ch|ss|sh)$', NULL), ('singular', '(matr)ix$', NULL), + ('singular', '(vert|ind)ex$', NULL), ('singular', '([m|l])ouse$', NULL), ('singular', '^foot$', NULL), ('singular', '^tooth$', NULL), @@ -417,11 +436,19 @@ INSERT INTO inflection.inflection_rules ( ('singular', '^(whereas)$', NULL), ('singular', '^(criteri)on$', NULL), ('singular', '^genus$', NULL), + -- singular replacement rules ('singular', '^(m|wom)en$', E'\\1an'), ('singular', '(pe)ople$', E'\\1rson'), ('singular', '(child)ren$', E'\\1'), + ('singular', '(database)s$', E'\\1'), + ('singular', '(drive)s$', E'\\1'), ('singular', '^genera$', 'genus'), ('singular', '^(criteri)a$', E'\\1on'), + -- Latin suffix overrides (PostGraphile-compatible) + ('singular', '(schema)ta$', E'\\1'), + ('singular', '(phenomen)a$', E'\\1on'), + ('singular', '(memorand)a$', E'\\1um'), + ('singular', '(curricul)a$', E'\\1um'), ('singular', '([ti])a$', E'\\1um'), ('singular', '((a)naly|(b)a|(d)iagno|(p)arenthe|(p)rogno|(s)ynop|(t)he)ses$', E'\\1\\2sis'), ('singular', '(hi|ti)ves$', E'\\1ve'), @@ -438,9 +465,9 @@ INSERT INTO inflection.inflection_rules ( ('singular', '(o)es$', E'\\1'), ('singular', '(shoe)s$', E'\\1'), ('singular', '(cris|ax|test)es$', E'\\1is'), - ('singular', '(octop|vir)i$', E'\\1us'), + ('singular', '(octop|vir)uses$', E'\\1us'), ('singular', '(alias|canvas|status|campus)es$', E'\\1'), - ('singular', '^(summons)es$', E'\\1'), + ('singular', '^(summons|bonus)es$', E'\\1'), ('singular', '^(ox)en', E'\\1'), ('singular', '(matr)ices$', E'\\1ix'), ('singular', '(vert|ind)ices$', E'\\1ex'), @@ -452,4 +479,4 @@ INSERT INTO inflection.inflection_rules ( ('singular', 'ss$', 'ss'), ('singular', 's$', ''); -CREATE INDEX inflection_rules_type_idx ON inflection.inflection_rules (type); \ No newline at end of file +CREATE INDEX inflection_rules_type_idx ON inflection.inflection_rules (type); diff --git a/packages/jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql b/packages/jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql index 4ad1305c9..dd003c348 100644 --- a/packages/jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql +++ b/packages/jobs/deploy/schemas/app_jobs/tables/job_queues/table.sql @@ -8,5 +8,12 @@ CREATE TABLE app_jobs.job_queues ( locked_at timestamptz, locked_by text ); + +COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue'; +COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue'; +COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue'; +COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing'; +COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock'; + COMMIT; diff --git a/packages/jobs/deploy/schemas/app_jobs/tables/jobs/table.sql b/packages/jobs/deploy/schemas/app_jobs/tables/jobs/table.sql index b692c2c57..667ae3a52 100644 --- a/packages/jobs/deploy/schemas/app_jobs/tables/jobs/table.sql +++ b/packages/jobs/deploy/schemas/app_jobs/tables/jobs/table.sql @@ -22,5 +22,20 @@ CREATE TABLE app_jobs.jobs ( CHECK (length(locked_by) > 3), UNIQUE (key) ); + +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue: each row is a pending or in-progress task to be executed by a worker'; +COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; +COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; +COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; +COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; +COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)'; +COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution'; +COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far'; +COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed'; +COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key'; +COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; +COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; +COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; + COMMIT; diff --git a/packages/jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql b/packages/jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql index 58d024189..c2ecc435b 100644 --- a/packages/jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql +++ b/packages/jobs/deploy/schemas/app_jobs/tables/scheduled_jobs/table.sql @@ -22,5 +22,20 @@ CREATE TABLE app_jobs.scheduled_jobs ( CHECK (length(locked_by) > 3), UNIQUE (key) ); + +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions: each row spawns jobs on a schedule defined by schedule_info'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule'; + COMMIT; diff --git a/packages/jobs/sql/pgpm-jobs--0.15.3.sql b/packages/jobs/sql/pgpm-jobs--0.15.3.sql index 653cc1178..4c287f144 100644 --- a/packages/jobs/sql/pgpm-jobs--0.15.3.sql +++ b/packages/jobs/sql/pgpm-jobs--0.15.3.sql @@ -133,6 +133,20 @@ CREATE TABLE app_jobs.scheduled_jobs ( UNIQUE (key) ); +COMMENT ON TABLE app_jobs.scheduled_jobs IS 'Recurring/cron-style job definitions: each row spawns jobs on a schedule defined by schedule_info'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.id IS 'Auto-incrementing scheduled job identifier'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.queue_name IS 'Name of the queue spawned jobs are placed into'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.task_identifier IS 'Task type identifier for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.payload IS 'JSON payload passed to each spawned job'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.priority IS 'Priority assigned to spawned jobs (lower = higher priority)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.max_attempts IS 'Max retry attempts for spawned jobs'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.key IS 'Optional unique deduplication key'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_at IS 'Timestamp when the scheduler locked this record for processing'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.locked_by IS 'Identifier of the scheduler worker holding the lock'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.schedule_info IS 'JSON schedule configuration (e.g. cron expression, interval)'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled IS 'Timestamp when a job was last spawned from this schedule'; +COMMENT ON COLUMN app_jobs.scheduled_jobs.last_scheduled_id IS 'ID of the last job spawned from this schedule'; + CREATE FUNCTION app_jobs.do_notify() RETURNS trigger AS $EOFCODE$ BEGIN PERFORM @@ -174,6 +188,20 @@ CREATE TABLE app_jobs.jobs ( UNIQUE (key) ); +COMMENT ON TABLE app_jobs.jobs IS 'Background job queue: each row is a pending or in-progress task to be executed by a worker'; +COMMENT ON COLUMN app_jobs.jobs.id IS 'Auto-incrementing job identifier'; +COMMENT ON COLUMN app_jobs.jobs.queue_name IS 'Name of the queue this job belongs to; used for worker routing and concurrency control'; +COMMENT ON COLUMN app_jobs.jobs.task_identifier IS 'Identifier for the task type (maps to a worker handler function)'; +COMMENT ON COLUMN app_jobs.jobs.payload IS 'JSON payload of arguments passed to the task handler'; +COMMENT ON COLUMN app_jobs.jobs.priority IS 'Execution priority; lower numbers run first (default 0)'; +COMMENT ON COLUMN app_jobs.jobs.run_at IS 'Earliest time this job should be executed; used for delayed/scheduled execution'; +COMMENT ON COLUMN app_jobs.jobs.attempts IS 'Number of times this job has been attempted so far'; +COMMENT ON COLUMN app_jobs.jobs.max_attempts IS 'Maximum retry attempts before the job is considered permanently failed'; +COMMENT ON COLUMN app_jobs.jobs.key IS 'Optional unique deduplication key; prevents duplicate jobs with the same key'; +COMMENT ON COLUMN app_jobs.jobs.last_error IS 'Error message from the most recent failed attempt'; +COMMENT ON COLUMN app_jobs.jobs.locked_at IS 'Timestamp when a worker locked this job for processing'; +COMMENT ON COLUMN app_jobs.jobs.locked_by IS 'Identifier of the worker that currently holds the lock'; + ALTER TABLE app_jobs.jobs ADD COLUMN created_at timestamptz; @@ -273,6 +301,12 @@ CREATE TABLE app_jobs.job_queues ( locked_by text ); +COMMENT ON TABLE app_jobs.job_queues IS 'Queue metadata: tracks job counts and locking state for each named queue'; +COMMENT ON COLUMN app_jobs.job_queues.queue_name IS 'Unique name identifying this queue'; +COMMENT ON COLUMN app_jobs.job_queues.job_count IS 'Number of pending jobs in this queue'; +COMMENT ON COLUMN app_jobs.job_queues.locked_at IS 'Timestamp when this queue was locked for batch processing'; +COMMENT ON COLUMN app_jobs.job_queues.locked_by IS 'Identifier of the worker that currently holds the queue lock'; + CREATE INDEX job_queues_locked_by_idx ON app_jobs.job_queues (locked_by); GRANT SELECT, INSERT, UPDATE, DELETE ON app_jobs.job_queues TO administrator; @@ -655,4 +689,4 @@ BEGIN * INTO v_job; RETURN v_job; END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; \ No newline at end of file +$EOFCODE$ LANGUAGE plpgsql VOLATILE SECURITY DEFINER; diff --git a/packages/jwt-claims/Makefile b/packages/jwt-claims/Makefile index e51a5ff3f..e3f7bef25 100644 --- a/packages/jwt-claims/Makefile +++ b/packages/jwt-claims/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-jwt-claims -DATA = sql/pgpm-jwt-claims--0.15.3.sql +DATA = sql/pgpm-jwt-claims--0.15.5.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/jwt-claims/deploy/schemas/jwt_private/procedures/current_session_id.sql b/packages/jwt-claims/deploy/schemas/jwt_private/procedures/current_session_id.sql new file mode 100644 index 000000000..ccf41ae0a --- /dev/null +++ b/packages/jwt-claims/deploy/schemas/jwt_private/procedures/current_session_id.sql @@ -0,0 +1,18 @@ +-- Deploy schemas/jwt_private/procedures/current_session_id to pg +-- Retrieves the current session ID from JWT claims (private/internal use) + +-- requires: schemas/jwt_private/schema + +BEGIN; + +-- Returns the current session UUID from the JWT claims +-- Used for session tracking, revocation, and audit logging +-- This is kept private to prevent session IDs from being exposed to the frontend +CREATE FUNCTION jwt_private.current_session_id() + RETURNS uuid +AS $$ + SELECT nullif(current_setting('jwt.claims.session_id', true), '')::uuid; +$$ +LANGUAGE 'sql' STABLE; + +COMMIT; diff --git a/packages/jwt-claims/pgpm-jwt-claims.control b/packages/jwt-claims/pgpm-jwt-claims.control index 3e3b2e2df..ebf219343 100644 --- a/packages/jwt-claims/pgpm-jwt-claims.control +++ b/packages/jwt-claims/pgpm-jwt-claims.control @@ -1,6 +1,6 @@ # pgpm-jwt-claims extension comment = 'pgpm-jwt-claims extension' -default_version = '0.15.3' +default_version = '0.15.5' module_pathname = '$libdir/pgpm-jwt-claims' requires = 'plpgsql,uuid-ossp,pgpm-types,pgpm-verify' relocatable = false diff --git a/packages/jwt-claims/pgpm.plan b/packages/jwt-claims/pgpm.plan index a95cc5f0e..e24acdc67 100644 --- a/packages/jwt-claims/pgpm.plan +++ b/packages/jwt-claims/pgpm.plan @@ -16,3 +16,4 @@ schemas/jwt_public/procedures/current_origin [schemas/jwt_public/schema] 2017-08 schemas/jwt_private/schema 2020-12-17T06:47:34Z Dan Lynch # add schemas/jwt_private/schema schemas/jwt_private/procedures/current_database_id [schemas/jwt_private/schema] 2020-12-17T23:22:28Z Dan Lynch # add schemas/jwt_private/procedures/current_database_id schemas/jwt_private/procedures/current_token_id [schemas/jwt_private/schema] 2017-08-11T08:11:51Z skitch # add schemas/jwt_private/procedures/current_token_id +schemas/jwt_private/procedures/current_session_id [schemas/jwt_private/schema] 2026-01-28T05:44:00Z Dan Lynch # add schemas/jwt_private/procedures/current_session_id diff --git a/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql new file mode 100644 index 000000000..fb07278e0 --- /dev/null +++ b/packages/jwt-claims/revert/schemas/jwt_private/procedures/current_session_id.sql @@ -0,0 +1,7 @@ +-- Revert schemas/jwt_private/procedures/current_session_id from pg + +BEGIN; + +DROP FUNCTION jwt_private.current_session_id; + +COMMIT; diff --git a/packages/jwt-claims/sql/pgpm-jwt-claims--0.15.5.sql b/packages/jwt-claims/sql/pgpm-jwt-claims--0.15.5.sql new file mode 100644 index 000000000..e2f1afb7e --- /dev/null +++ b/packages/jwt-claims/sql/pgpm-jwt-claims--0.15.5.sql @@ -0,0 +1,147 @@ +\echo Use "CREATE EXTENSION pgpm-jwt-claims" to load this file. \quit +CREATE SCHEMA ctx; + +GRANT USAGE ON SCHEMA ctx TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA ctx + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION ctx.ip_address() RETURNS inet AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.ip_address', true), '')::inet; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION ctx.origin() RETURNS origin AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.origin', true), '')::origin; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION ctx.uagent() RETURNS text AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.user_agent', true), ''); +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION ctx.uid() RETURNS uuid AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.user_id', true), '')::uuid; +$EOFCODE$ LANGUAGE sql STABLE; + +DO $EOFCODE$ + DECLARE + BEGIN + EXECUTE format('CREATE FUNCTION ctx.security_definer() returns text as $FUNC$ + SELECT ''%s''; +$FUNC$ +LANGUAGE ''sql'';', current_user); + EXECUTE format('CREATE FUNCTION ctx.is_security_definer() returns bool as $FUNC$ + SELECT ''%s'' = current_user; +$FUNC$ +LANGUAGE ''sql'';', current_user); + END; +$EOFCODE$; + +GRANT EXECUTE ON FUNCTION ctx.security_definer() TO PUBLIC; + +GRANT EXECUTE ON FUNCTION ctx.is_security_definer() TO PUBLIC; + +CREATE SCHEMA jwt_public; + +GRANT USAGE ON SCHEMA jwt_public TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA jwt_public + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION jwt_public.current_user_id() RETURNS uuid AS $EOFCODE$ +DECLARE + v_identifier_id uuid; +BEGIN + IF current_setting('jwt.claims.user_id', TRUE) + IS NOT NULL THEN + BEGIN + v_identifier_id = current_setting('jwt.claims.user_id', TRUE)::uuid; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UUID value'; + RETURN NULL; + END; + RETURN v_identifier_id; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_public.current_ip_address() RETURNS inet AS $EOFCODE$ +DECLARE + v_ip_addr inet; +BEGIN + IF current_setting('jwt.claims.ip_address', TRUE) + IS NOT NULL THEN + BEGIN + v_ip_addr = trim(current_setting('jwt.claims.ip_address', TRUE))::inet; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid IP'; + RETURN NULL; + END; + RETURN v_ip_addr; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_public.current_user_agent() RETURNS text AS $EOFCODE$ +DECLARE + v_uagent text; +BEGIN + IF current_setting('jwt.claims.user_agent', TRUE) + IS NOT NULL THEN + BEGIN + v_uagent = current_setting('jwt.claims.user_agent', TRUE); + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UserAgent'; + RETURN NULL; + END; + RETURN v_uagent; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_public.current_origin() RETURNS origin AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.origin', true), '')::origin; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE SCHEMA jwt_private; + +GRANT USAGE ON SCHEMA jwt_private TO authenticated, anonymous; + +ALTER DEFAULT PRIVILEGES IN SCHEMA jwt_private + GRANT EXECUTE ON FUNCTIONS TO authenticated; + +CREATE FUNCTION jwt_private.current_database_id() RETURNS uuid AS $EOFCODE$ +DECLARE + v_identifier_id uuid; +BEGIN + IF current_setting('jwt.claims.database_id', TRUE) + IS NOT NULL THEN + BEGIN + v_identifier_id = current_setting('jwt.claims.database_id', TRUE)::uuid; + EXCEPTION + WHEN OTHERS THEN + RAISE NOTICE 'Invalid UUID value'; + RETURN NULL; + END; + RETURN v_identifier_id; + ELSE + RETURN NULL; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql STABLE; + +CREATE FUNCTION jwt_private.current_token_id() RETURNS uuid AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.token_id', true), '')::uuid; +$EOFCODE$ LANGUAGE sql STABLE; + +CREATE FUNCTION jwt_private.current_session_id() RETURNS uuid AS $EOFCODE$ + SELECT nullif(current_setting('jwt.claims.session_id', true), '')::uuid; +$EOFCODE$ LANGUAGE sql STABLE; diff --git a/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql new file mode 100644 index 000000000..8eebd8340 --- /dev/null +++ b/packages/jwt-claims/verify/schemas/jwt_private/procedures/current_session_id.sql @@ -0,0 +1,7 @@ +-- Verify schemas/jwt_private/procedures/current_session_id on pg + +BEGIN; + +SELECT verify_function ('jwt_private.current_session_id'); + +ROLLBACK; diff --git a/packages/measurements/deploy/schemas/measurements/tables/quantities/table.sql b/packages/measurements/deploy/schemas/measurements/tables/quantities/table.sql index 0a6a65a4e..3988d89ce 100644 --- a/packages/measurements/deploy/schemas/measurements/tables/quantities/table.sql +++ b/packages/measurements/deploy/schemas/measurements/tables/quantities/table.sql @@ -13,4 +13,12 @@ CREATE TABLE measurements.quantities ( description text ); +COMMENT ON TABLE measurements.quantities IS 'Unit of measure definitions: maps quantity names to their display labels, units, and descriptions'; +COMMENT ON COLUMN measurements.quantities.id IS 'Auto-incrementing identifier for this quantity'; +COMMENT ON COLUMN measurements.quantities.name IS 'Machine-readable name for this quantity (e.g. length, mass, temperature)'; +COMMENT ON COLUMN measurements.quantities.label IS 'Human-readable display label'; +COMMENT ON COLUMN measurements.quantities.unit IS 'Unit symbol or abbreviation (e.g. m, kg, °C)'; +COMMENT ON COLUMN measurements.quantities.unit_desc IS 'Full unit name (e.g. meters, kilograms, degrees Celsius)'; +COMMENT ON COLUMN measurements.quantities.description IS 'Detailed description of what this quantity measures'; + COMMIT; diff --git a/packages/measurements/sql/pgpm-measurements--0.15.3.sql b/packages/measurements/sql/pgpm-measurements--0.15.3.sql index 0dc786469..b9306df74 100644 --- a/packages/measurements/sql/pgpm-measurements--0.15.3.sql +++ b/packages/measurements/sql/pgpm-measurements--0.15.3.sql @@ -10,6 +10,14 @@ CREATE TABLE measurements.quantities ( description text ); +COMMENT ON TABLE measurements.quantities IS 'Unit of measure definitions: maps quantity names to their display labels, units, and descriptions'; +COMMENT ON COLUMN measurements.quantities.id IS 'Auto-incrementing identifier for this quantity'; +COMMENT ON COLUMN measurements.quantities.name IS 'Machine-readable name for this quantity (e.g. length, mass, temperature)'; +COMMENT ON COLUMN measurements.quantities.label IS 'Human-readable display label'; +COMMENT ON COLUMN measurements.quantities.unit IS 'Unit symbol or abbreviation (e.g. m, kg, °C)'; +COMMENT ON COLUMN measurements.quantities.unit_desc IS 'Full unit name (e.g. meters, kilograms, degrees Celsius)'; +COMMENT ON COLUMN measurements.quantities.description IS 'Detailed description of what this quantity measures'; + INSERT INTO measurements.quantities ( id, name, @@ -73,4 +81,4 @@ INSERT INTO measurements.quantities ( ) VALUES (45, 'Percent', 'Percent', '%', 'percentage', 'a number or ratio expressed as a fraction of 100'), (46, 'PartsPerMillion', 'Parts per Million', 'ppm', 'parts per million', 'pseudo-units to describe small values of miscellaneous dimensionless quantities that are pure numbers representing a quantity-per-quantity measure in parts per million'), - (47, 'PartsPerBillion', 'Parts per Billion', 'ppb', 'parts per billion', 'pseudo-units to describe small values of miscellaneous dimensionless quantities that are pure numbers representing a quantity-per-quantity measure in parts per billion'); \ No newline at end of file + (47, 'PartsPerBillion', 'Parts per Billion', 'ppb', 'parts per billion', 'pseudo-units to describe small values of miscellaneous dimensionless quantities that are pure numbers representing a quantity-per-quantity measure in parts per billion'); diff --git a/packages/metaschema-modules/README.md b/packages/metaschema-modules/README.md index b80d42f60..27e2c8226 100644 --- a/packages/metaschema-modules/README.md +++ b/packages/metaschema-modules/README.md @@ -1,4 +1,4 @@ -# @pgpm/metaschema-modules +# @pgpm/db-meta-modules

@@ -9,14 +9,14 @@ - +

Module metadata handling and dependency tracking. ## Overview -`@pgpm/metaschema-modules` extends the `@pgpm/metaschema-schema` package with module-specific metadata tables. This package provides tables for tracking various pgpm modules including authentication, permissions, memberships, encrypted secrets, and more. It enables configuration and metadata storage for modular application features. +`@pgpm/db-meta-modules` extends the `@pgpm/db-meta-schema` package with module-specific metadata tables. This package provides tables for tracking various pgpm modules including authentication, permissions, memberships, encrypted secrets, and more. It enables configuration and metadata storage for modular application features. ## Features @@ -33,7 +33,7 @@ Module metadata handling and dependency tracking. If you have `pgpm` installed: ```bash -pgpm install @pgpm/metaschema-modules +pgpm install @pgpm/db-meta-modules pgpm deploy ``` @@ -56,7 +56,7 @@ eval "$(pgpm env)" ```bash # 1. Install the package -pgpm install @pgpm/metaschema-modules +pgpm install @pgpm/db-meta-modules # 2. Deploy locally pgpm deploy @@ -74,7 +74,7 @@ pgpm init # 3. Install a package cd packages/my-module -pgpm install @pgpm/metaschema-modules +pgpm install @pgpm/db-meta-modules # 4. Deploy everything pgpm deploy --createdb --database mydb1 @@ -213,8 +213,7 @@ Use module tables as feature flags: ## Dependencies -- `@pgpm/metaschema-schema`: Core metadata management -- `@pgpm/services`: Services schemas for APIs, sites, and domains +- `@pgpm/db-meta-schema`: Core metadata management - `@pgpm/verify`: Verification utilities ## Testing diff --git a/packages/metaschema-modules/__tests__/modules.test.ts b/packages/metaschema-modules/__tests__/modules.test.ts index e202d0aac..26720406b 100644 --- a/packages/metaschema-modules/__tests__/modules.test.ts +++ b/packages/metaschema-modules/__tests__/modules.test.ts @@ -224,7 +224,7 @@ describe('db_meta_modules', () => { constraintCount: fkConstraints.length, foreignTables: foreignTables.sort() })).toMatchSnapshot(); - }, 30000); + }); it('should verify specific module table column defaults', async () => { // Check that modules have sensible defaults diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/field_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/field_module/table.sql index dd6c431d8..afb49ad06 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/field_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/field_module/table.sql @@ -13,8 +13,14 @@ CREATE TABLE metaschema_modules_public.field_module ( table_id uuid NOT NULL DEFAULT uuid_nil(), field_id uuid NOT NULL DEFAULT uuid_nil(), + -- Node type from node_type_registry (e.g., 'FieldSlug', 'FieldImmutable', 'FieldInflection', 'FieldOwned') node_type text NOT NULL, + -- Type-specific parameters as jsonb + -- FieldSlug: {"source_field_id": "uuid"} + -- FieldImmutable: {} (no extra params) + -- FieldInflection: {"ops": ["snake_case", "uppercase"]} + -- FieldOwned: {"role_key_field_id": "uuid", "protected_field_ids": ["uuid", ...]} data jsonb NOT NULL DEFAULT '{}', triggers text[], @@ -35,4 +41,3 @@ CREATE INDEX field_module_database_id_idx ON metaschema_modules_public.field_mod CREATE INDEX field_module_node_type_idx ON metaschema_modules_public.field_module ( node_type ); COMMIT; - diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql index 73754b2f4..7c6f8b4c0 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -27,8 +27,6 @@ CREATE TABLE metaschema_modules_public.profiles_module ( profile_definition_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), profile_definition_grants_table_name text NOT NULL DEFAULT '', - -- Configuration - bitlen int NOT NULL DEFAULT 24, membership_type int NOT NULL, -- Entity table for org/group scoped profiles (NULL for app-level) diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql new file mode 100644 index 000000000..0692c49cd --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql @@ -0,0 +1,282 @@ +-- Deploy schemas/metaschema_modules_public/tables/relation_provision/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.relation_provision ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), + + database_id uuid NOT NULL, + + -- ========================================================================= + -- Relation type and tables + -- ========================================================================= + + relation_type text NOT NULL CHECK (relation_type IN ( + 'RelationBelongsTo', 'RelationHasOne', 'RelationHasMany', 'RelationManyToMany' + )), + + source_table_id uuid NOT NULL, + + target_table_id uuid NOT NULL, + + -- ========================================================================= + -- BelongsTo / HasOne / HasMany: FK field config + -- ========================================================================= + + field_name text DEFAULT NULL, + + delete_action text DEFAULT NULL, + + is_required boolean NOT NULL DEFAULT true, + + -- ========================================================================= + -- ManyToMany: junction table identity + -- ========================================================================= + + junction_table_id uuid NOT NULL DEFAULT uuid_nil(), + + junction_table_name text DEFAULT NULL, + + junction_schema_id uuid DEFAULT NULL, + + source_field_name text DEFAULT NULL, + + target_field_name text DEFAULT NULL, + + -- ========================================================================= + -- ManyToMany: junction table primary key strategy + -- ========================================================================= + + use_composite_key boolean NOT NULL DEFAULT false, + + -- ========================================================================= + -- ManyToMany: field creation (forwarded to secure_table_provision) + -- ========================================================================= + + node_type text DEFAULT NULL, + + node_data jsonb NOT NULL DEFAULT '{}', + + -- ========================================================================= + -- ManyToMany: grants (forwarded to secure_table_provision) + -- ========================================================================= + + grant_roles text[] NOT NULL DEFAULT ARRAY['authenticated'], + + grant_privileges jsonb NOT NULL DEFAULT '[["select","*"],["insert","*"],["delete","*"]]', + + -- ========================================================================= + -- ManyToMany: RLS policies (forwarded to secure_table_provision) + -- ========================================================================= + + policy_type text DEFAULT NULL, + + policy_privileges text[] DEFAULT NULL, + + policy_role text DEFAULT NULL, + + policy_permissive boolean NOT NULL DEFAULT true, + + policy_data jsonb NOT NULL DEFAULT '{}', + + -- ========================================================================= + -- Output columns (populated by the trigger, not set by callers) + -- ========================================================================= + + out_field_id uuid DEFAULT NULL, + + out_junction_table_id uuid DEFAULT NULL, + + out_source_field_id uuid DEFAULT NULL, + + out_target_field_id uuid DEFAULT NULL, + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT source_table_fkey FOREIGN KEY (source_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT target_table_fkey FOREIGN KEY (target_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +-- ============================================================================= +-- Table-level comment +-- ============================================================================= + +COMMENT ON TABLE metaschema_modules_public.relation_provision IS + 'Provisions relational structure between tables. Supports four relation types: + - RelationBelongsTo: adds a FK field on the source table referencing the target table (child perspective: "tasks belongs to projects" -> tasks.project_id). + - RelationHasMany: adds a FK field on the target table referencing the source table (parent perspective: "projects has many tasks" -> tasks.project_id). Inverse of BelongsTo. + - RelationHasOne: adds a FK field with a unique constraint on the source table referencing the target table. Also supports shared-primary-key patterns where the FK field IS the primary key (set field_name to the existing PK field name). + - RelationManyToMany: creates a junction table with FK fields to both source and target tables, delegating table creation and security to secure_table_provision. + This is a one-and-done structural provisioner. To layer additional security onto junction tables after creation, use secure_table_provision directly. + All operations are graceful: existing fields, FK constraints, and unique constraints are reused if found. + The trigger never injects values the caller did not provide. All security config is forwarded to secure_table_provision as-is.'; + +-- ============================================================================= +-- Relation type and tables +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.id IS + 'Unique identifier for this relation provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.database_id IS + 'The database this relation belongs to. Required. Must match the database of both source_table_id and target_table_id.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.relation_type IS + 'The type of relation to create. Uses SuperCase naming matching the node_type_registry: + - RelationBelongsTo: creates a FK field on source_table referencing target_table (e.g., tasks belongs to projects -> tasks.project_id). Field name auto-derived from target table. + - RelationHasMany: creates a FK field on target_table referencing source_table (e.g., projects has many tasks -> tasks.project_id). Field name auto-derived from source table. Inverse of BelongsTo — same FK, different perspective. + - RelationHasOne: creates a FK field + unique constraint on source_table referencing target_table (e.g., user_settings has one user -> user_settings.user_id with UNIQUE). Also supports shared-primary-key patterns (e.g., user_profiles.id = users.id) by setting field_name to the existing PK field. + - RelationManyToMany: creates a junction table with FK fields to both tables (e.g., projects and tags -> project_tags table). + Each relation type uses a different subset of columns on this table. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_table_id IS + 'The source table in the relation. Required. + - RelationBelongsTo: the table that receives the FK field (e.g., tasks in "tasks belongs to projects"). + - RelationHasMany: the parent table being referenced (e.g., projects in "projects has many tasks"). The FK field is created on the target table. + - RelationHasOne: the table that receives the FK field + unique constraint (e.g., user_settings in "user_settings has one user"). + - RelationManyToMany: one of the two tables being joined (e.g., projects in "projects and tags"). The junction table will have a FK field referencing this table.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_table_id IS + 'The target table in the relation. Required. + - RelationBelongsTo: the table being referenced by the FK (e.g., projects in "tasks belongs to projects"). + - RelationHasMany: the table that receives the FK field (e.g., tasks in "projects has many tasks"). + - RelationHasOne: the table being referenced by the FK (e.g., users in "user_settings has one user"). + - RelationManyToMany: the other table being joined (e.g., tags in "projects and tags"). The junction table will have a FK field referencing this table.'; + +-- ============================================================================= +-- BelongsTo / HasOne / HasMany: FK field config +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.field_name IS + 'FK field name for RelationBelongsTo, RelationHasOne, and RelationHasMany. + - RelationBelongsTo/RelationHasOne: if NULL, auto-derived from the target table name (e.g., target "projects" derives "project_id"). + - RelationHasMany: if NULL, auto-derived from the source table name (e.g., source "projects" derives "project_id"). + For RelationHasOne shared-primary-key patterns, set field_name to the existing PK field (e.g., "id") so the FK reuses it. + Ignored for RelationManyToMany — use source_field_name/target_field_name instead.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.delete_action IS + 'FK delete action for RelationBelongsTo, RelationHasOne, and RelationHasMany. One of: c (CASCADE), r (RESTRICT), n (SET NULL), d (SET DEFAULT), a (NO ACTION). Required — the trigger raises an error if not provided. The caller must explicitly choose the cascade behavior; there is no default. Ignored for RelationManyToMany (junction FK fields always use CASCADE).'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.is_required IS + 'Whether the FK field is NOT NULL. Defaults to true. + - RelationBelongsTo: set to false for optional associations (e.g., tasks.assignee_id that can be NULL). + - RelationHasMany: set to false if the child can exist without a parent. + - RelationHasOne: typically true. + Ignored for RelationManyToMany (junction FK fields are always required).'; + +-- ============================================================================= +-- ManyToMany: junction table identity +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_id IS + 'For RelationManyToMany: an existing junction table to use. Defaults to uuid_nil(). + - When uuid_nil(): the trigger creates a new junction table via secure_table_provision using junction_table_name. + - When set to a valid table UUID: the trigger skips table creation and only adds FK fields, composite key (if use_composite_key is true), and security to the existing table. + Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_name IS + 'For RelationManyToMany: name of the junction table to create or look up. If NULL, auto-derived from source and target table names using inflection_db (e.g., "projects" + "tags" derives "project_tags"). Only used when junction_table_id is uuid_nil(). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_schema_id IS + 'For RelationManyToMany: schema for the junction table. If NULL, defaults to the source table''s schema. Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_field_name IS + 'For RelationManyToMany: FK field name on the junction table referencing the source table. If NULL, auto-derived from the source table name using inflection_db.get_foreign_key_field_name() (e.g., source table "projects" derives "project_id"). Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_field_name IS + 'For RelationManyToMany: FK field name on the junction table referencing the target table. If NULL, auto-derived from the target table name using inflection_db.get_foreign_key_field_name() (e.g., target table "tags" derives "tag_id"). Ignored for RelationBelongsTo/RelationHasOne.'; + +-- ============================================================================= +-- ManyToMany: junction table primary key strategy +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.use_composite_key IS + 'For RelationManyToMany: whether to create a composite primary key from the two FK fields (source + target) on the junction table. Defaults to false. + - When true: the trigger calls metaschema.pk() with ARRAY[source_field_id, target_field_id] to create a composite PK. No separate id column is created. This enforces uniqueness of the pair and is suitable for simple junction tables. + - When false: no primary key is created by the trigger. The caller should provide node_type=''DataId'' to create a UUID primary key, or handle the PK strategy via a separate secure_table_provision row. + use_composite_key and node_type=''DataId'' are mutually exclusive — using both would create two conflicting PKs. + Ignored for RelationBelongsTo/RelationHasOne.'; + +-- ============================================================================= +-- ManyToMany: field creation (forwarded to secure_table_provision) +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.node_type IS + 'For RelationManyToMany: which generator to invoke for field creation on the junction table. Forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. + Examples: DataId (creates UUID primary key), DataDirectOwner (creates owner_id field), DataEntityMembership (creates entity_id field), DataOwnershipInEntity (creates both owner_id and entity_id), DataTimestamps, DataPeoplestamps, DataPublishable, DataSoftDelete. + NULL means no field creation beyond the FK fields (and composite key if use_composite_key is true). + Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.node_data IS + 'For RelationManyToMany: configuration passed to the generator function for field creation on the junction table. Forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. + Only used when node_type is set. Structure varies by node_type. Examples: + - DataId: {"field_name": "id"} (default field name is ''id'') + - DataEntityMembership: {"entity_field_name": "entity_id", "include_id": false, "include_user_fk": true} + - DataDirectOwner: {"owner_field_name": "owner_id"} + Defaults to ''{}'' (empty object). + Ignored for RelationBelongsTo/RelationHasOne.'; + +-- ============================================================================= +-- ManyToMany: grants (forwarded to secure_table_provision) +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.grant_roles IS + 'For RelationManyToMany: database roles to grant privileges to on the junction table. Forwarded to secure_table_provision as-is. Supports multiple roles, e.g. ARRAY[''authenticated'', ''admin'']. Each role receives all privileges defined in grant_privileges. Defaults to ARRAY[''authenticated'']. Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.grant_privileges IS + 'For RelationManyToMany: privilege grants for the junction table. Forwarded to secure_table_provision as-is. Format: array of [privilege, columns] tuples. Examples: [["select","*"],["insert","*"]] for full access, or [["update",["name","bio"]]] for column-level grants. "*" means all columns. Defaults to select/insert/delete for all columns. Ignored for RelationBelongsTo/RelationHasOne.'; + +-- ============================================================================= +-- ManyToMany: RLS policies (forwarded to secure_table_provision) +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_type IS + 'For RelationManyToMany: RLS policy type for the junction table. Forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. + Examples: AuthzEntityMembership, AuthzMembership, AuthzAllowAll, AuthzDirectOwner, AuthzOrgHierarchy. + NULL means no policy is created — the junction table will have RLS enabled but no policies (unless added separately via secure_table_provision). + Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_privileges IS + 'For RelationManyToMany: privileges the policy applies to, e.g. ARRAY[''select'',''insert'',''delete'']. Forwarded to secure_table_provision as-is. NULL means privileges are derived from the grant_privileges verbs by secure_table_provision. Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_role IS + 'For RelationManyToMany: database role the policy targets, e.g. ''authenticated''. Forwarded to secure_table_provision as-is. NULL means secure_table_provision falls back to the first role in grant_roles. Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_permissive IS + 'For RelationManyToMany: whether the policy is PERMISSIVE (true) or RESTRICTIVE (false). Forwarded to secure_table_provision as-is. Defaults to true. Ignored for RelationBelongsTo/RelationHasOne.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_data IS + 'For RelationManyToMany: opaque policy configuration forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. Structure varies by policy_type. Examples: + - AuthzEntityMembership: {"entity_field": "entity_id", "membership_type": 2} + - AuthzDirectOwner: {"owner_field": "owner_id"} + - AuthzMembership: {"membership_type": 2} + Defaults to ''{}'' (empty object). + Ignored for RelationBelongsTo/RelationHasOne.'; + +-- ============================================================================= +-- Output columns +-- ============================================================================= + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_field_id IS + 'Output column for RelationBelongsTo/RelationHasOne/RelationHasMany: the UUID of the FK field created (or found). For BelongsTo/HasOne this is on the source table; for HasMany this is on the target table. Populated by the trigger. NULL for RelationManyToMany. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_junction_table_id IS + 'Output column for RelationManyToMany: the UUID of the junction table created (or found). Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_source_field_id IS + 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the source table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_target_field_id IS + 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the target table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.relation_provision IS E'@omit manyToMany'; +COMMENT ON CONSTRAINT source_table_fkey ON metaschema_modules_public.relation_provision IS E'@omit manyToMany'; +COMMENT ON CONSTRAINT target_table_fkey ON metaschema_modules_public.relation_provision IS E'@omit manyToMany'; + +CREATE INDEX relation_provision_database_id_idx ON metaschema_modules_public.relation_provision ( database_id ); +CREATE INDEX relation_provision_relation_type_idx ON metaschema_modules_public.relation_provision ( relation_type ); +CREATE INDEX relation_provision_source_table_id_idx ON metaschema_modules_public.relation_provision ( source_table_id ); +CREATE INDEX relation_provision_target_table_id_idx ON metaschema_modules_public.relation_provision ( target_table_id ); + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql index 2cc8ec9e0..61828349d 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql @@ -12,7 +12,8 @@ CREATE TABLE metaschema_modules_public.rls_module ( api_id uuid NOT NULL DEFAULT uuid_nil(), schema_id uuid NOT NULL DEFAULT uuid_nil(), private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), users_table_id uuid NOT NULL DEFAULT uuid_nil(), -- @@ -25,7 +26,8 @@ CREATE TABLE metaschema_modules_public.rls_module ( -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT api_fkey FOREIGN KEY (api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE, - CONSTRAINT tokens_table_fkey FOREIGN KEY (tokens_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, CONSTRAINT pschema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -39,7 +41,8 @@ COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.rls_module IS E'@ COMMENT ON CONSTRAINT pschema_fkey ON metaschema_modules_public.rls_module IS E'@omit manyToMany'; COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.rls_module IS E'@omit'; -COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; CREATE INDEX rls_module_database_id_idx ON metaschema_modules_public.rls_module ( database_id ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql index b4b360a36..532d2d514 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql @@ -7,24 +7,27 @@ BEGIN; CREATE TABLE metaschema_modules_public.table_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), database_id uuid NOT NULL, - - private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - - table_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text DEFAULT NULL, node_type text NOT NULL, + use_rls boolean NOT NULL DEFAULT true, + data jsonb NOT NULL DEFAULT '{}', fields uuid[], - -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE ); -COMMENT ON CONSTRAINT private_schema_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; CREATE INDEX table_module_database_id_idx ON metaschema_modules_public.table_module ( database_id ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_template_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_template_module/table.sql index 06deb600d..dde40ceab 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_template_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_template_module/table.sql @@ -16,8 +16,13 @@ CREATE TABLE metaschema_modules_public.table_template_module ( table_name text NOT NULL, + -- Node type from node_type_registry (e.g., 'TableUserProfiles', 'TableOrganizationSettings', 'TableUserSettings') node_type text NOT NULL, + -- Type-specific parameters as jsonb + -- TableUserProfiles: {} (uses default fields) + -- TableOrganizationSettings: {} (uses default fields) + -- TableUserSettings: {} (uses default fields) data jsonb NOT NULL DEFAULT '{}', -- diff --git a/packages/metaschema-modules/pgpm.plan b/packages/metaschema-modules/pgpm.plan index 4cc7e2218..1c0b81bb4 100644 --- a/packages/metaschema-modules/pgpm.plan +++ b/packages/metaschema-modules/pgpm.plan @@ -32,4 +32,5 @@ schemas/metaschema_modules_public/tables/users_module/table [schemas/metaschema_ schemas/metaschema_modules_public/tables/uuid_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/uuid_module/table schemas/metaschema_modules_public/tables/hierarchy_module/table [schemas/metaschema_modules_public/schema] 2024-12-28T00:00:00Z skitch # add schemas/metaschema_modules_public/tables/hierarchy_module/table schemas/metaschema_modules_public/tables/table_template_module/table [schemas/metaschema_modules_public/schema] 2026-01-14T00:00:00Z devin # add schemas/metaschema_modules_public/tables/table_template_module/table -schemas/metaschema_modules_public/tables/secure_table_provision/table [schemas/metaschema_modules_public/schema] 2026-02-26T00:00:00Z devin # add schemas/metaschema_modules_public/tables/secure_table_provision/table +schemas/metaschema_modules_public/tables/secure_table_provision/table [schemas/metaschema_modules_public/schema] 2026-02-25T00:00:00Z Constructive # add schemas/metaschema_modules_public/tables/secure_table_provision/table +schemas/metaschema_modules_public/tables/relation_provision/table [schemas/metaschema_modules_public/schema] 2026-02-26T00:00:00Z Constructive # add schemas/metaschema_modules_public/tables/relation_provision/table diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql index afd1bb13e..27924803a 100644 --- a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -DROP TABLE metaschema_modules_public.field_module; +DROP TABLE IF EXISTS metaschema_modules_public.field_module; COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql new file mode 100644 index 000000000..d85cc87d5 --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/relation_provision/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.relation_provision; + +COMMIT; diff --git a/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql b/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql index 5107a1f97..2f0041fef 100644 --- a/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql +++ b/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql @@ -37,11 +37,11 @@ CREATE TABLE metaschema_modules_public.connected_accounts_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -80,11 +80,11 @@ CREATE TABLE metaschema_modules_public.crypto_addresses_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -113,8 +113,9 @@ CREATE TABLE metaschema_modules_public.crypto_auth_module ( database_id uuid NOT NULL, schema_id uuid NOT NULL DEFAULT uuid_nil(), users_table_id uuid NOT NULL DEFAULT uuid_nil(), - tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), addresses_table_id uuid NOT NULL DEFAULT uuid_nil(), user_field text NOT NULL, crypto_network text NOT NULL DEFAULT 'BTC', @@ -128,15 +129,19 @@ CREATE TABLE metaschema_modules_public.crypto_auth_module ( ON DELETE CASCADE, CONSTRAINT secrets_table_fkey FOREIGN KEY(secrets_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, - CONSTRAINT tokens_table_fkey - FOREIGN KEY(tokens_table_id) - REFERENCES metaschema_public.table (id) + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -150,7 +155,9 @@ COMMENT ON CONSTRAINT secrets_table_fkey ON metaschema_modules_public.crypto_aut COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; -COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; @@ -188,11 +195,11 @@ CREATE TABLE metaschema_modules_public.denormalized_table_field ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT ref_table_fkey FOREIGN KEY(ref_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT field_fkey FOREIGN KEY(field_id) @@ -230,11 +237,11 @@ CREATE TABLE metaschema_modules_public.emails_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -274,7 +281,7 @@ CREATE TABLE metaschema_modules_public.encrypted_secrets_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -292,6 +299,7 @@ CREATE TABLE metaschema_modules_public.field_module ( private_schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), field_id uuid NOT NULL DEFAULT uuid_nil(), + node_type text NOT NULL, data jsonb NOT NULL DEFAULT '{}', triggers text[], functions text[], @@ -301,7 +309,7 @@ CREATE TABLE metaschema_modules_public.field_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT field_fkey FOREIGN KEY(field_id) @@ -323,6 +331,140 @@ COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.field_module IS '@omi CREATE INDEX field_module_database_id_idx ON metaschema_modules_public.field_module (database_id); +CREATE INDEX field_module_node_type_idx ON metaschema_modules_public.field_module (node_type); + +CREATE TABLE metaschema_modules_public.table_module ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text DEFAULT NULL, + node_type text NOT NULL, + use_rls boolean NOT NULL DEFAULT true, + data jsonb NOT NULL DEFAULT '{}', + fields uuid[], + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.table_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.table_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.table_module IS '@omit manyToMany'; + +CREATE INDEX table_module_database_id_idx ON metaschema_modules_public.table_module (database_id); + +CREATE INDEX table_module_table_id_idx ON metaschema_modules_public.table_module (table_id); + +CREATE INDEX table_module_node_type_idx ON metaschema_modules_public.table_module (node_type); + +CREATE TABLE metaschema_modules_public.secure_table_provision ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text DEFAULT NULL, + node_type text DEFAULT NULL, + use_rls boolean NOT NULL DEFAULT true, + node_data jsonb NOT NULL DEFAULT '{}', + grant_roles text[] NOT NULL DEFAULT ARRAY['authenticated'], + grant_privileges jsonb NOT NULL DEFAULT '[]', + policy_type text DEFAULT NULL, + policy_privileges text[] DEFAULT NULL, + policy_role text DEFAULT NULL, + policy_permissive boolean NOT NULL DEFAULT true, + policy_data jsonb NOT NULL DEFAULT '{}', + out_fields uuid[] DEFAULT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE metaschema_modules_public.secure_table_provision IS 'Provisions security, fields, grants, and policies onto a table. Each row can independently: (1) create fields via node_type, (2) grant privileges via grant_privileges, (3) create RLS policies via policy_type. Multiple rows can target the same table to compose different concerns. All three concerns are optional and independent.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.id IS 'Unique identifier for this provision row.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.database_id IS 'The database this provision belongs to. Required.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.schema_id IS 'Target schema for the table. Defaults to uuid_nil(); the trigger resolves this to the app_public schema if not explicitly provided.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_id IS 'Target table to provision. Defaults to uuid_nil(); the trigger creates or resolves the table via table_name if not explicitly provided.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_name IS 'Name of the target table. Used to create or look up the table when table_id is not provided. If omitted, it is backfilled from the resolved table.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.node_type IS 'Which generator to invoke for field creation. One of: DataId, DataDirectOwner, DataEntityMembership, DataOwnershipInEntity, DataTimestamps, DataPeoplestamps, DataPublishable, DataSoftDelete. NULL means no field creation — the row only provisions grants and/or policies.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.use_rls IS 'If true and Row Level Security is not yet enabled on the target table, enable it. Automatically set to true by the trigger when policy_type is provided. Defaults to true.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.node_data IS 'Configuration passed to the generator function for field creation (only used when node_type is set). Known keys include: field_name (text, default ''id'') for DataId, owner_field_name (text, default ''owner_id'') for DataDirectOwner/DataOwnershipInEntity, entity_field_name (text, default ''entity_id'') for DataEntityMembership/DataOwnershipInEntity, include_id (boolean, default true) for most node_types, include_user_fk (boolean, default true) to add FK to users table. Defaults to ''{}''.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.grant_roles IS 'Database roles to grant privileges to. Supports multiple roles, e.g. ARRAY[''authenticated'', ''admin'']. Each role receives all privileges defined in grant_privileges. Defaults to ARRAY[''authenticated''].'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.grant_privileges IS 'Array of [privilege, columns] tuples defining table grants. Examples: [["select","*"],["insert","*"]] for full access, or [["update",["name","bio"]]] for column-level grants. "*" means all columns; an array means column-level grant. Defaults to ''[]'' (no grants). The trigger validates this is a proper jsonb array.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_type IS 'Policy generator type, e.g. ''AuthzEntityMembership'', ''AuthzMembership'', ''AuthzAllowAll''. NULL means no policy is created. When set, the trigger automatically enables RLS on the target table.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_privileges IS 'Privileges the policy applies to, e.g. ARRAY[''select'',''update'']. NULL means privileges are derived from the grant_privileges verbs.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_role IS 'Role the policy targets. NULL means it falls back to the first role in grant_roles.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_permissive IS 'Whether the policy is PERMISSIVE (true) or RESTRICTIVE (false). Defaults to true.'; + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_data IS 'Opaque configuration passed through to metaschema.create_policy(). Structure varies by policy_type and is not interpreted by this trigger. Defaults to ''{}''.'; + + +COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.out_fields IS 'Output column populated by the trigger after field creation. Contains the UUIDs of the metaschema fields created on the target table by this provision row''s generator. NULL when node_type is NULL or before the trigger runs. Callers should not set this directly.'; + +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.secure_table_provision IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.secure_table_provision IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.secure_table_provision IS '@omit manyToMany'; + +CREATE INDEX secure_table_provision_database_id_idx ON metaschema_modules_public.secure_table_provision (database_id); + +CREATE INDEX secure_table_provision_table_id_idx ON metaschema_modules_public.secure_table_provision (table_id); + +CREATE INDEX secure_table_provision_node_type_idx ON metaschema_modules_public.secure_table_provision (node_type); + +CREATE FUNCTION metaschema_modules_private.tg_set_secure_table_provision_deterministic_id() RETURNS trigger AS $EOFCODE$ +BEGIN + IF current_setting('metaschema.deterministic_ids', true) = 'true' THEN + NEW.id := metaschema_private.deterministic_id(NEW.table_id, NEW.node_type); + END IF; + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql; + +CREATE TRIGGER zzz_set_deterministic_id + BEFORE INSERT + ON metaschema_modules_public.secure_table_provision + FOR EACH ROW + EXECUTE PROCEDURE metaschema_modules_private.tg_set_secure_table_provision_deterministic_id(); + +ALTER TABLE metaschema_modules_public.secure_table_provision + ENABLE ALWAYS TRIGGER zzz_set_deterministic_id; + CREATE TABLE metaschema_modules_public.invites_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), database_id uuid NOT NULL, @@ -344,23 +486,23 @@ CREATE TABLE metaschema_modules_public.invites_module ( ON DELETE CASCADE, CONSTRAINT invites_table_fkey FOREIGN KEY(invites_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT emails_table_fkey FOREIGN KEY(emails_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT claimed_invites_table_fkey FOREIGN KEY(claimed_invites_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -429,27 +571,27 @@ CREATE TABLE metaschema_modules_public.levels_module ( ON DELETE CASCADE, CONSTRAINT steps_table_fkey FOREIGN KEY(steps_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT achievements_table_fkey FOREIGN KEY(achievements_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT levels_table_fkey FOREIGN KEY(levels_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT level_requirements_table_fkey FOREIGN KEY(level_requirements_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -502,19 +644,19 @@ CREATE TABLE metaschema_modules_public.limits_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT default_table_fkey FOREIGN KEY(default_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -548,7 +690,7 @@ CREATE TABLE metaschema_modules_public.membership_types_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -606,27 +748,27 @@ CREATE TABLE metaschema_modules_public.memberships_module ( ON DELETE CASCADE, CONSTRAINT memberships_table_fkey FOREIGN KEY(memberships_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT membership_defaults_table_fkey FOREIGN KEY(membership_defaults_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT members_table_fkey FOREIGN KEY(members_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT grants_table_fkey FOREIGN KEY(grants_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT sprt_table_fkey FOREIGN KEY(sprt_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_owner_fkey FOREIGN KEY(entity_table_owner_id) @@ -634,23 +776,23 @@ CREATE TABLE metaschema_modules_public.memberships_module ( ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT limits_table_fkey FOREIGN KEY(limits_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT default_limits_table_fkey FOREIGN KEY(default_limits_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT permissions_table_fkey FOREIGN KEY(permissions_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT default_permissions_table_fkey FOREIGN KEY(default_permissions_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -718,19 +860,19 @@ CREATE TABLE metaschema_modules_public.permissions_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT default_table_fkey FOREIGN KEY(default_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -762,11 +904,11 @@ CREATE TABLE metaschema_modules_public.phone_numbers_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -803,7 +945,6 @@ CREATE TABLE metaschema_modules_public.profiles_module ( profile_grants_table_name text NOT NULL DEFAULT '', profile_definition_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), profile_definition_grants_table_name text NOT NULL DEFAULT '', - bitlen int NOT NULL DEFAULT 24, membership_type int NOT NULL, entity_table_id uuid NULL, actor_table_id uuid NOT NULL DEFAULT uuid_nil(), @@ -824,35 +965,35 @@ CREATE TABLE metaschema_modules_public.profiles_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT profile_permissions_table_fkey FOREIGN KEY(profile_permissions_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT profile_grants_table_fkey FOREIGN KEY(profile_grants_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT profile_definition_grants_table_fkey FOREIGN KEY(profile_definition_grants_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT permissions_table_fkey FOREIGN KEY(permissions_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT memberships_table_fkey FOREIGN KEY(memberships_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT profiles_module_unique UNIQUE (database_id, membership_type) @@ -888,7 +1029,8 @@ CREATE TABLE metaschema_modules_public.rls_module ( api_id uuid NOT NULL DEFAULT uuid_nil(), schema_id uuid NOT NULL DEFAULT uuid_nil(), private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), users_table_id uuid NOT NULL DEFAULT uuid_nil(), authenticate text NOT NULL DEFAULT 'authenticate', authenticate_strict text NOT NULL DEFAULT 'authenticate_strict', @@ -902,13 +1044,17 @@ CREATE TABLE metaschema_modules_public.rls_module ( FOREIGN KEY(api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE, - CONSTRAINT tokens_table_fkey - FOREIGN KEY(tokens_table_id) - REFERENCES metaschema_public.table (id) + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -930,7 +1076,9 @@ COMMENT ON CONSTRAINT pschema_fkey ON metaschema_modules_public.rls_module IS '@ COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.rls_module IS '@omit'; -COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; + +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; @@ -952,7 +1100,7 @@ CREATE TABLE metaschema_modules_public.secrets_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -964,14 +1112,18 @@ CREATE INDEX secrets_module_database_id_idx ON metaschema_modules_public.secrets COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.secrets_module IS '@omit manyToMany'; -CREATE TABLE metaschema_modules_public.tokens_module ( +CREATE TABLE metaschema_modules_public.sessions_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), database_id uuid NOT NULL, schema_id uuid NOT NULL DEFAULT uuid_nil(), - table_id uuid NOT NULL DEFAULT uuid_nil(), - owned_table_id uuid NOT NULL DEFAULT uuid_nil(), - tokens_default_expiration interval NOT NULL DEFAULT '3 days'::interval, - tokens_table text NOT NULL DEFAULT 'api_tokens', + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + auth_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + users_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_default_expiration interval NOT NULL DEFAULT '30 days'::interval, + sessions_table text NOT NULL DEFAULT 'sessions', + session_credentials_table text NOT NULL DEFAULT 'session_credentials', + auth_settings_table text NOT NULL DEFAULT 'app_auth_settings', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -980,25 +1132,40 @@ CREATE TABLE metaschema_modules_public.tokens_module ( FOREIGN KEY(schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, - CONSTRAINT table_fkey - FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, - CONSTRAINT owned_table_fkey - FOREIGN KEY(owned_table_id) - REFERENCES metaschema_public.table (id) + CONSTRAINT auth_settings_table_fkey + FOREIGN KEY(auth_settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT users_table_fkey + FOREIGN KEY(users_table_id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); -COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.sessions_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.sessions_module IS '@omit manyToMany'; -COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; +CREATE INDEX sessions_module_database_id_idx ON metaschema_modules_public.sessions_module (database_id); -CREATE INDEX tokens_module_database_id_idx ON metaschema_modules_public.tokens_module (database_id); +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName sessionsTableBySessionsTableId +@omit manyToMany'; -COMMENT ON CONSTRAINT owned_table_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName sessionCredentialsTableBySessionCredentialsTableId +@omit manyToMany'; -COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; +COMMENT ON CONSTRAINT auth_settings_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName authSettingsTableByAuthSettingsTableId +@omit manyToMany'; + +COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.sessions_module IS '@omit manyToMany'; CREATE TABLE metaschema_modules_public.user_auth_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -1008,7 +1175,8 @@ CREATE TABLE metaschema_modules_public.user_auth_module ( users_table_id uuid NOT NULL DEFAULT uuid_nil(), secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), encrypted_table_id uuid NOT NULL DEFAULT uuid_nil(), - tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), + sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), audits_table_id uuid NOT NULL DEFAULT uuid_nil(), audits_table_name text NOT NULL DEFAULT 'audit_logs', sign_in_function text NOT NULL DEFAULT 'sign_in', @@ -1036,23 +1204,27 @@ CREATE TABLE metaschema_modules_public.user_auth_module ( ON DELETE CASCADE, CONSTRAINT email_table_fkey FOREIGN KEY(emails_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT secrets_table_fkey FOREIGN KEY(secrets_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT encrypted_table_fkey FOREIGN KEY(encrypted_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, - CONSTRAINT tokens_table_fkey - FOREIGN KEY(tokens_table_id) - REFERENCES metaschema_public.table (id) + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -1070,7 +1242,9 @@ COMMENT ON CONSTRAINT secrets_table_fkey ON metaschema_modules_public.user_auth_ COMMENT ON CONSTRAINT encrypted_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; -COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; +COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; + +COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; CREATE TABLE metaschema_modules_public.users_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -1090,11 +1264,11 @@ CREATE TABLE metaschema_modules_public.users_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT type_table_fkey FOREIGN KEY(type_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -1165,23 +1339,23 @@ CREATE TABLE metaschema_modules_public.hierarchy_module ( ON DELETE CASCADE, CONSTRAINT chart_edges_table_fkey FOREIGN KEY(chart_edges_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT hierarchy_sprt_table_fkey FOREIGN KEY(hierarchy_sprt_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT chart_edge_grants_table_fkey FOREIGN KEY(chart_edge_grants_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public.table (id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT hierarchy_module_database_unique UNIQUE (database_id) @@ -1203,4 +1377,58 @@ COMMENT ON CONSTRAINT chart_edge_grants_table_fkey ON metaschema_modules_public. COMMENT ON CONSTRAINT entity_table_fkey ON metaschema_modules_public.hierarchy_module IS '@omit manyToMany'; -COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.hierarchy_module IS '@omit manyToMany'; \ No newline at end of file +COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.hierarchy_module IS '@omit manyToMany'; + +CREATE TABLE metaschema_modules_public.table_template_module ( + id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL, + node_type text NOT NULL, + data jsonb NOT NULL DEFAULT '{}', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT owner_table_fkey + FOREIGN KEY(owner_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE +); + +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT private_schema_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT owner_table_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; + +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; + +CREATE INDEX table_template_module_database_id_idx ON metaschema_modules_public.table_template_module (database_id); + +CREATE INDEX table_template_module_schema_id_idx ON metaschema_modules_public.table_template_module (schema_id); + +CREATE INDEX table_template_module_private_schema_id_idx ON metaschema_modules_public.table_template_module (private_schema_id); + +CREATE INDEX table_template_module_table_id_idx ON metaschema_modules_public.table_template_module (table_id); + +CREATE INDEX table_template_module_owner_table_id_idx ON metaschema_modules_public.table_template_module (owner_table_id); + +CREATE INDEX table_template_module_node_type_idx ON metaschema_modules_public.table_template_module (node_type); diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql index 1255ee8ac..94cb11ed3 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql @@ -2,6 +2,8 @@ BEGIN; -SELECT verify_table ('metaschema_modules_public.field_module'); +SELECT id, database_id, private_schema_id, table_id, field_id, node_type, data, triggers, functions +FROM metaschema_modules_public.field_module +WHERE FALSE; ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql index 65fa1b25c..64810749e 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -6,7 +6,7 @@ SELECT id, database_id, schema_id, private_schema_id, table_id, table_name, profile_permissions_table_id, profile_permissions_table_name, profile_grants_table_id, profile_grants_table_name, profile_definition_grants_table_id, profile_definition_grants_table_name, - bitlen, membership_type, entity_table_id, actor_table_id, + membership_type, entity_table_id, actor_table_id, permissions_table_id, memberships_table_id, prefix FROM metaschema_modules_public.profiles_module WHERE FALSE; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql new file mode 100644 index 000000000..188f14db2 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql @@ -0,0 +1,36 @@ +-- Verify schemas/metaschema_modules_public/tables/relation_provision/table on pg + +BEGIN; + +SELECT + id, + database_id, + relation_type, + source_table_id, + target_table_id, + field_name, + delete_action, + is_required, + junction_table_id, + junction_table_name, + junction_schema_id, + source_field_name, + target_field_name, + use_composite_key, + node_type, + node_data, + grant_roles, + grant_privileges, + policy_type, + policy_privileges, + policy_role, + policy_permissive, + policy_data, + out_field_id, + out_junction_table_id, + out_source_field_id, + out_target_field_id +FROM metaschema_modules_public.relation_provision +WHERE FALSE; + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql index e94672491..80cd415b0 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql @@ -5,9 +5,11 @@ BEGIN; SELECT id, database_id, - private_schema_id, + schema_id, table_id, + table_name, node_type, + use_rls, data, fields FROM metaschema_modules_public.table_module diff --git a/packages/metaschema-schema/README.md b/packages/metaschema-schema/README.md index 5a5e9c14f..854a5a63f 100644 --- a/packages/metaschema-schema/README.md +++ b/packages/metaschema-schema/README.md @@ -1,4 +1,4 @@ -# @pgpm/metaschema-schema +# @pgpm/db-meta-schema

@@ -9,14 +9,14 @@ - +

Database metadata utilities and introspection functions. ## Overview -`@pgpm/metaschema-schema` provides a comprehensive metadata management system for PostgreSQL databases. This package creates tables and schemas for storing and querying database structure information including databases, schemas, tables, fields, constraints, indexes, and more. It enables runtime schema introspection, metadata-driven code generation, and database structure management. +`@pgpm/db-meta-schema` provides a comprehensive metadata management system for PostgreSQL databases. This package creates tables and schemas for storing and querying database structure information including databases, schemas, tables, fields, constraints, indexes, and more. It enables runtime schema introspection, metadata-driven code generation, and database structure management. ## Features @@ -25,6 +25,7 @@ Database metadata utilities and introspection functions. - **Index Management**: Store and query index definitions - **Trigger and Procedure Metadata**: Track database functions and triggers - **RLS and Policy Information**: Store row-level security policies +- **Extension Tracking**: Manage database extensions and their relationships - **API and Site Metadata**: Store API configurations and site information - **GraphQL Integration**: Smart tags and annotations for GraphQL schema generation @@ -33,7 +34,7 @@ Database metadata utilities and introspection functions. If you have `pgpm` installed: ```bash -pgpm install @pgpm/metaschema-schema +pgpm install @pgpm/db-meta-schema pgpm deploy ``` @@ -56,7 +57,7 @@ eval "$(pgpm env)" ```bash # 1. Install the package -pgpm install @pgpm/metaschema-schema +pgpm install @pgpm/db-meta-schema # 2. Deploy locally pgpm deploy @@ -74,7 +75,7 @@ pgpm init # 3. Install a package cd packages/my-module -pgpm install @pgpm/metaschema-schema +pgpm install @pgpm/db-meta-schema # 4. Deploy everything pgpm deploy --createdb --database mydb1 @@ -98,6 +99,8 @@ Stores database structure metadata: - **trigger**: Trigger definitions - **procedure**: Stored procedure definitions - **policy**: Row-level security policies +- **extension**: PostgreSQL extensions +- **database_extension**: Extension installations per database ### metaschema_private Schema @@ -108,6 +111,7 @@ Private schema for internal metadata operations. Application-level metadata: - **apis**: API configurations +- **api_extensions**: API extension relationships - **api_modules**: API module definitions - **api_schemas**: API schema configurations - **sites**: Site definitions diff --git a/packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap b/packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap new file mode 100644 index 000000000..fbb35cce5 --- /dev/null +++ b/packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`db_meta functionality should handle complete meta workflow 1`] = ` +{ + "hash": null, + "id": "[ID]", + "label": null, + "name": "my-meta-db", + "owner_id": "[ID]", + "private_schema_name": null, + "schema_hash": null, + "schema_name": null, +} +`; + +exports[`db_meta functionality should handle complete meta workflow 2`] = ` +{ + "anon_role": "anonymous", + "database_id": "[ID]", + "dbname": "test-database", + "id": "[ID]", + "is_public": true, + "name": "public", + "role_name": "authenticated", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 3`] = ` +{ + "anon_role": "administrator", + "database_id": "[ID]", + "dbname": "test-database", + "id": "[ID]", + "is_public": true, + "name": "admin", + "role_name": "administrator", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 4`] = ` +{ + "apple_touch_icon": null, + "database_id": "[ID]", + "dbname": "test-database", + "description": "Website Description", + "favicon": null, + "id": "[ID]", + "logo": null, + "og_image": null, + "title": "Website Title", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 5`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": null, + "subdomain": "api", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 6`] = ` +{ + "api_id": null, + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": "[ID]", + "subdomain": "app", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 7`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": null, + "subdomain": "admin", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 8`] = ` +{ + "data": { + "supportEmail": "support@interweb.co", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "legal-emails", + "site_id": "[ID]", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 9`] = ` +{ + "api_id": "[ID]", + "data": { + "authenticate": "authenticate", + "authenticate_schema": "services_private", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "rls_module", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 10`] = ` +{ + "data": { + "auth_schema": "services_public", + "forgot_password": "forgot_password", + "reset_password": "reset_password", + "send_verification_email": "send_verification_email", + "set_password": "set_password", + "sign_in": "login", + "sign_up": "register", + "verify_email": "verify_email", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "user_auth_module", + "site_id": "[ID]", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 11`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "id": "[ID]", + "schema_id": "[ID]", +} +`; + +exports[`db_meta functionality should handle complete meta workflow 12`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "id": "[ID]", + "schema_id": "[ID]", +} +`; diff --git a/packages/metaschema-schema/__tests__/meta.test.ts b/packages/metaschema-schema/__tests__/meta.test.ts index 51614e20c..fe622fa33 100644 --- a/packages/metaschema-schema/__tests__/meta.test.ts +++ b/packages/metaschema-schema/__tests__/meta.test.ts @@ -1,9 +1,9 @@ -import { getConnections, PgTestClient } from 'pgsql-test'; +import { getConnections, PgTestClient, snapshot } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; -describe('metaschema_schema functionality', () => { +describe('db_meta functionality', () => { beforeAll(async () => { ({ pg, teardown } = await getConnections()); }); @@ -14,6 +14,7 @@ describe('metaschema_schema functionality', () => { beforeEach(async () => { await pg.beforeEach(); + // Grant execute permissions for functions await pg.any(`GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO public`); }); @@ -21,6 +22,172 @@ describe('metaschema_schema functionality', () => { await pg.afterEach(); }); + it('should handle complete meta workflow', async () => { + const objs: Record = { + tables: {}, + domains: {}, + apis: {}, + sites: {} + }; + + const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; + + // Helper function for snapshots + const snap = (obj: any) => { + expect(snapshot(obj)).toMatchSnapshot(); + }; + + // Helper function for snapshots with dbname normalization + const snapWithNormalizedDbname = (obj: any) => { + const normalized = { + ...obj, + dbname: 'test-database' // Replace dynamic dbname with static value + }; + expect(snapshot(normalized)).toMatchSnapshot(); + }; + + // Step 1: Create database + const [database] = await pg.any( + `INSERT INTO metaschema_public.database (owner_id, name) + VALUES ($1, $2) + RETURNING *`, + [owner_id, 'my-meta-db'] + ); + objs.db = database; + const database_id = database.id; + expect(snapshot(database)).toMatchSnapshot(); + + // Step 2: Create APIs first (since domains reference them) + const [publicApi] = await pg.any( + `INSERT INTO services_public.apis (database_id, name, role_name, anon_role) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, 'public', 'authenticated', 'anonymous'] + ); + objs.apis.public = publicApi; + snapWithNormalizedDbname(publicApi); + + const [adminApi] = await pg.any( + `INSERT INTO services_public.apis (database_id, name, role_name, anon_role) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, 'admin', 'administrator', 'administrator'] + ); + objs.apis.admin = adminApi; + snapWithNormalizedDbname(adminApi); + + // Step 3: Create sites + const [appSite] = await pg.any( + `INSERT INTO services_public.sites (database_id, title, description) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, 'Website Title', 'Website Description'] + ); + objs.sites.app = appSite; + snapWithNormalizedDbname(appSite); + + // Step 4: Register domains (linking to APIs and sites) + const [apiDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, api_id, domain, subdomain) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, objs.apis.public.id, 'pgpm.io', 'api'] + ); + objs.domains.api = apiDomain; + expect(snapshot(apiDomain)).toMatchSnapshot(); + + const [appDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, site_id, domain, subdomain) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, objs.sites.app.id, 'pgpm.io', 'app'] + ); + objs.domains.app = appDomain; + expect(snapshot(appDomain)).toMatchSnapshot(); + + const [adminDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, api_id, domain, subdomain) + VALUES ($1, $2, $3, $4) + RETURNING *`, + [database_id, objs.apis.admin.id, 'pgpm.io', 'admin'] + ); + objs.domains.admin = adminDomain; + expect(snapshot(adminDomain)).toMatchSnapshot(); + + const [baseDomain] = await pg.any( + `INSERT INTO services_public.domains (database_id, domain) + VALUES ($1, $2) + RETURNING *`, + [database_id, 'pgpm.io'] + ); + objs.domains.base = baseDomain; + + // Step 5: Register modules + const [siteModule1] = await pg.any( + `INSERT INTO services_public.site_modules (database_id, site_id, name, data) + VALUES ($1, $2, $3, $4::jsonb) + RETURNING *`, + [database_id, objs.sites.app.id, 'legal-emails', JSON.stringify({ + supportEmail: 'support@interweb.co' + })] + ); + expect(snapshot(siteModule1)).toMatchSnapshot(); + + const [apiModule] = await pg.any( + `INSERT INTO services_public.api_modules (database_id, api_id, name, data) + VALUES ($1, $2, $3, $4::jsonb) + RETURNING *`, + [database_id, objs.apis.public.id, 'rls_module', JSON.stringify({ + authenticate_schema: 'services_private', + authenticate: 'authenticate' + })] + ); + expect(snapshot(apiModule)).toMatchSnapshot(); + + const [siteModule2] = await pg.any( + `INSERT INTO services_public.site_modules (database_id, site_id, name, data) + VALUES ($1, $2, $3, $4::jsonb) + RETURNING *`, + [database_id, objs.sites.app.id, 'user_auth_module', JSON.stringify({ + auth_schema: 'services_public', + sign_in: 'login', + sign_up: 'register', + set_password: 'set_password', + reset_password: 'reset_password', + forgot_password: 'forgot_password', + send_verification_email: 'send_verification_email', + verify_email: 'verify_email' + })] + ); + expect(snapshot(siteModule2)).toMatchSnapshot(); + + // Step 6: Schema associations + const [schema] = await pg.any( + `INSERT INTO metaschema_public.schema (database_id, schema_name, name) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, 'brand-public', 'public'] + ); + + const [publicAssoc] = await pg.any( + `INSERT INTO services_public.api_schemas (database_id, schema_id, api_id) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, schema.id, objs.apis.public.id] + ); + + const [adminAssoc] = await pg.any( + `INSERT INTO services_public.api_schemas (database_id, schema_id, api_id) + VALUES ($1, $2, $3) + RETURNING *`, + [database_id, schema.id, objs.apis.admin.id] + ); + + snap(publicAssoc); + snap(adminAssoc); + }); + + // Individual component tests it('should create database independently', async () => { const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; @@ -35,4 +202,28 @@ describe('metaschema_schema functionality', () => { expect(database.name).toBe('test-db'); expect(database.id).toBeDefined(); }); + + it('should register domain independently', async () => { + const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; + + // Create database first + const [database] = await pg.any( + `INSERT INTO metaschema_public.database (owner_id, name) + VALUES ($1, $2) + RETURNING *`, + [owner_id, 'test-db-for-domain'] + ); + + // Then create domain + const [domain] = await pg.any( + `INSERT INTO services_public.domains (database_id, domain, subdomain) + VALUES ($1, $2, $3) + RETURNING *`, + [database.id, 'example.com', 'api'] + ); + + expect(domain.database_id).toBe(database.id); + expect(domain.domain).toBe('example.com'); + expect(domain.subdomain).toBe('api'); + }); }); diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql index fca5045a4..c9299c373 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/database/table.sql @@ -8,23 +8,17 @@ CREATE TABLE metaschema_public.database ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), owner_id uuid, schema_hash text, - schema_name text, - private_schema_name text, name text, label text, hash uuid, - unique(schema_hash), - unique(schema_name), - unique(private_schema_name) + unique(schema_hash) ); ALTER TABLE metaschema_public.database ADD CONSTRAINT db_namechk CHECK (char_length(name) > 2); COMMENT ON COLUMN metaschema_public.database.schema_hash IS '@omit'; --- COMMENT ON COLUMN metaschema_public.database.schema_name IS '@omit'; --- COMMENT ON COLUMN metaschema_public.database.private_schema_name IS '@omit'; COMMIT; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql index e1851a39a..a2e08484d 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql @@ -3,10 +3,25 @@ -- requires: schemas/metaschema_public/schema -- requires: schemas/metaschema_public/tables/table/table --- requires: schemas/metaschema_public/types/object_category BEGIN; +-- TODO should we just query this table and make a view? +-- https://www.postgresql.org/docs/9.2/catalog-pg-attribute.html + +-- IF YOU WANT TO REMOVE THIS TABLE, answer the qustion, how would you add RLS to this: +-- SELECT +-- attrelid::text AS tbl +-- , attname::text AS col +-- , p.attnum::int as id, +-- t.typname as typename + +-- FROM pg_catalog.pg_attribute p +-- INNER JOIN pg_catalog.pg_type t ON (t.oid = p.atttypid) +-- WHERE attrelid = 'dude_schema.products'::regclass +-- AND p.attnum > 0 +-- AND NOT attisdropped; + CREATE TABLE metaschema_public.field ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), database_id uuid NOT NULL DEFAULT uuid_nil(), @@ -41,11 +56,14 @@ CREATE TABLE metaschema_public.field ( tags citext[] NOT NULL DEFAULT '{}', + -- Field categorization for system/module/app fields (mirrors table categorization) + -- category: 'core' for system fields (id, entity_id, actor_id), 'module' for module-generated fields, 'app' for user-defined fields + -- module: the module name that created this field (e.g., 'users', 'permissions', 'memberships') + -- scope: membership_type int (1=app, 2=org, 3=group, NULL=not scoped) category metaschema_public.object_category NOT NULL DEFAULT 'app', module text NULL, scope int NULL, - -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/foreign_key_constraint/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/foreign_key_constraint/table.sql index f6b0bc8e0..14d5a1e4c 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/foreign_key_constraint/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/foreign_key_constraint/table.sql @@ -19,7 +19,7 @@ CREATE TABLE metaschema_public.foreign_key_constraint ( field_ids uuid[] NOT NULL, ref_table_id uuid NOT NULL REFERENCES metaschema_public.table (id) ON DELETE CASCADE, ref_field_ids uuid[] NOT NULL, - delete_action char(1) DEFAULT 'c', + delete_action char(1) DEFAULT 'c', -- postgres default is 'a' update_action char(1) DEFAULT 'a', category metaschema_public.object_category NOT NULL DEFAULT 'app', diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/index/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/index/table.sql index f575950ca..320d44b1a 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/index/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/index/table.sql @@ -33,6 +33,7 @@ CREATE TABLE metaschema_public.index ( CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + -- index names are UNIQUE across schemas, so for portability we will check against database_id UNIQUE (database_id, name) ); diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/policy/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/policy/table.sql index e3ea79fcd..3a78cf476 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/policy/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/policy/table.sql @@ -15,6 +15,10 @@ CREATE TABLE metaschema_public.policy ( role_name text, privilege text, + -- using_expression text, + -- check_expression text, + -- policy_text text, + permissive boolean default true, disabled boolean default false, diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/procedure/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/procedure/table.sql index 701ff7b2a..d9eea8f78 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/procedure/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/procedure/table.sql @@ -12,6 +12,9 @@ CREATE TABLE metaschema_public.procedure ( name text NOT NULL, + -- MAYBE MAKE A SPECIAL RLS functions for policy making... + + -- can we make this all JSON? argnames text[], argtypes text[], argdefaults text[], 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 cccf5d5e8..f22c7f0c6 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 @@ -31,6 +31,9 @@ CREATE TABLE metaschema_public.schema ( UNIQUE (schema_name) ); +-- TODO: build out services +-- COMMENT ON COLUMN metaschema_public.schema.schema_name IS '@omit'; + ALTER TABLE metaschema_public.schema ADD CONSTRAINT schema_namechk CHECK (char_length(name) > 2); diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/table/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/table/table.sql index e5d8bfdf0..71a75a07a 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/table/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/table/table.sql @@ -33,8 +33,6 @@ CREATE TABLE metaschema_public.table ( tags citext[] NOT NULL DEFAULT '{}', - -- - CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger/table.sql index 0258142a7..c38e79c0b 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/trigger/table.sql @@ -6,13 +6,15 @@ BEGIN; +-- https://www.postgresql.org/docs/12/sql-createtrigger.html + CREATE TABLE metaschema_public.trigger ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), database_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL, name text NOT NULL, - event text, + event text, -- INSERT, UPDATE, DELETE, or TRUNCATE function_name text, smart_tags jsonb, diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/unique_constraint/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/unique_constraint/table.sql index 6497989bb..ff46166aa 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/unique_constraint/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/unique_constraint/table.sql @@ -27,6 +27,9 @@ CREATE TABLE metaschema_public.unique_constraint ( CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + -- TODO these are unique across schema, NOT table. We'll need to update this to have database_id + -- for portability + UNIQUE (table_id, name), CHECK (field_ids <> '{}') ); 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 ea77e21ec..9480d9f52 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 @@ -4,6 +4,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.) +-- 'app' - user-defined application objects CREATE TYPE metaschema_public.object_category AS ENUM ('core', 'module', 'app'); COMMIT; diff --git a/packages/metaschema-schema/pgpm.plan b/packages/metaschema-schema/pgpm.plan index 41952e02a..947eb5b30 100644 --- a/packages/metaschema-schema/pgpm.plan +++ b/packages/metaschema-schema/pgpm.plan @@ -4,28 +4,27 @@ 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] 2026-01-14T00:00:00Z devin # add schemas/metaschema_public/types/object_category +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/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/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 schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/check_constraint/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 -schemas/metaschema_public/tables/field/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/field/table +schemas/metaschema_public/tables/field/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/field/table schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx [schemas/metaschema_public/schema schemas/metaschema_public/tables/field/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx -schemas/metaschema_public/tables/foreign_key_constraint/table [schemas/metaschema_public/tables/field/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/schema schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/foreign_key_constraint/table +schemas/metaschema_public/tables/foreign_key_constraint/table [schemas/metaschema_public/tables/field/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/foreign_key_constraint/table schemas/metaschema_public/tables/full_text_search/table [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/full_text_search/table -schemas/metaschema_public/tables/index/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/database/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/index/table +schemas/metaschema_public/tables/index/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/index/table schemas/metaschema_public/tables/limit_function/table [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/limit_function/table -schemas/metaschema_public/tables/policy/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/policy/table -schemas/metaschema_public/tables/primary_key_constraint/table [schemas/metaschema_public/schema schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/primary_key_constraint/table -schemas/metaschema_public/tables/procedure/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/procedure/table -schemas/metaschema_public/tables/rls_function/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/rls_function/table +schemas/metaschema_public/tables/policy/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/policy/table +schemas/metaschema_public/tables/primary_key_constraint/table [schemas/metaschema_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/primary_key_constraint/table +schemas/metaschema_public/tables/procedure/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/procedure/table schemas/metaschema_public/tables/schema_grant/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/schema/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/schema_grant/table schemas/metaschema_public/tables/table_grant/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/table_grant/table schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx [schemas/metaschema_public/schema schemas/metaschema_private/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/table/indexes/databases_table_unique_name_idx schemas/metaschema_public/tables/trigger_function/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/trigger_function/table -schemas/metaschema_public/tables/trigger/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/trigger/table -schemas/metaschema_public/tables/unique_constraint/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/types/object_category] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/unique_constraint/table +schemas/metaschema_public/tables/trigger/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/table/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_public/tables/trigger/table +schemas/metaschema_public/tables/unique_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/unique_constraint/table schemas/metaschema_public/tables/view/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/schema/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/database/table schemas/metaschema_public/types/object_category] 2026-01-23T00:00:00Z devin # add schemas/metaschema_public/tables/view/table schemas/metaschema_public/tables/view_table/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/view/table schemas/metaschema_public/tables/table/table] 2026-01-23T00:00:00Z devin # add schemas/metaschema_public/tables/view_table/table schemas/metaschema_public/tables/view_grant/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/view/table schemas/metaschema_public/tables/database/table] 2026-01-23T00:00:00Z devin # add schemas/metaschema_public/tables/view_grant/table diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/types/object_category.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/types/object_category.sql index 84ba74ed2..68174dd43 100644 --- a/packages/metaschema-schema/revert/schemas/metaschema_public/types/object_category.sql +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/types/object_category.sql @@ -2,6 +2,6 @@ BEGIN; -DROP TYPE IF EXISTS metaschema_public.object_category; +DROP TYPE metaschema_public.object_category; COMMIT; diff --git a/packages/metaschema-schema/sql/metaschema-schema--0.15.5.sql b/packages/metaschema-schema/sql/metaschema-schema--0.15.5.sql index ee9766734..97d86e491 100644 --- a/packages/metaschema-schema/sql/metaschema-schema--0.15.5.sql +++ b/packages/metaschema-schema/sql/metaschema-schema--0.15.5.sql @@ -25,18 +25,16 @@ 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 TABLE metaschema_public.database ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), owner_id uuid, schema_hash text, - schema_name text, - private_schema_name text, name text, label text, hash uuid, - UNIQUE (schema_hash), - UNIQUE (schema_name), - UNIQUE (private_schema_name) + UNIQUE (schema_hash) ); ALTER TABLE metaschema_public.database @@ -52,6 +50,12 @@ CREATE TABLE metaschema_public.schema ( schema_name text NOT NULL, label text, description text, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + is_public boolean NOT NULL DEFAULT true, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -68,8 +72,6 @@ COMMENT ON CONSTRAINT db_fkey ON metaschema_public.schema IS '@omit manyToMany'; CREATE INDEX schema_database_id_idx ON metaschema_public.schema (database_id); -CREATE TYPE metaschema_public.table_category AS ENUM ('core', 'module', 'app'); - CREATE TABLE metaschema_public."table" ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), database_id uuid NOT NULL DEFAULT uuid_nil(), @@ -78,7 +80,7 @@ CREATE TABLE metaschema_public."table" ( label text, description text, smart_tags jsonb, - category metaschema_public.table_category NOT NULL DEFAULT 'app', + category metaschema_public.object_category NOT NULL DEFAULT 'app', module text NULL, scope int NULL, use_rls boolean NOT NULL DEFAULT false, @@ -119,6 +121,11 @@ CREATE TABLE metaschema_public.check_constraint ( type text, field_ids uuid[] NOT NULL, expr jsonb, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -146,8 +153,6 @@ $EOFCODE$ LANGUAGE sql IMMUTABLE; CREATE UNIQUE INDEX databases_database_unique_name_idx ON metaschema_public.database (owner_id, (metaschema_private.database_name_hash(name))); -CREATE TYPE metaschema_public.field_category AS ENUM ('core', 'module', 'app'); - CREATE TABLE metaschema_public.field ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), database_id uuid NOT NULL DEFAULT uuid_nil(), @@ -168,7 +173,7 @@ CREATE TABLE metaschema_public.field ( min double precision DEFAULT NULL, max double precision DEFAULT NULL, tags citext[] NOT NULL DEFAULT '{}', - category metaschema_public.field_category NOT NULL DEFAULT 'app', + category metaschema_public.object_category NOT NULL DEFAULT 'app', module text NULL, scope int NULL, CONSTRAINT db_fkey @@ -192,7 +197,10 @@ CREATE INDEX field_database_id_idx ON metaschema_public.field (database_id); COMMENT ON COLUMN metaschema_public.field.default_value IS '@sqlExpression'; -CREATE UNIQUE INDEX databases_field_uniq_names_idx ON metaschema_public.field (table_id, (decode(md5(lower(CASE WHEN type = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', E'\\1', 'i') ELSE name END)), 'hex'))); +CREATE UNIQUE INDEX databases_field_uniq_names_idx ON metaschema_public.field (table_id, (decode(md5(lower(CASE + WHEN type = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', E'\\1', 'i') + ELSE name +END)), 'hex'))); CREATE TABLE metaschema_public.foreign_key_constraint ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -208,6 +216,10 @@ CREATE TABLE metaschema_public.foreign_key_constraint ( ref_field_ids uuid[] NOT NULL, delete_action char(1) DEFAULT 'c', update_action char(1) DEFAULT 'a', + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -270,6 +282,11 @@ CREATE TABLE metaschema_public.index ( index_params jsonb, where_clause jsonb, is_unique boolean NOT NULL DEFAULT false, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -326,8 +343,13 @@ CREATE TABLE metaschema_public.policy ( privilege text, permissive boolean DEFAULT true, disabled boolean DEFAULT false, - template text, + policy_type text, data jsonb, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -354,6 +376,11 @@ CREATE TABLE metaschema_public.primary_key_constraint ( name text, type text, field_ids uuid[] NOT NULL, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -383,6 +410,11 @@ CREATE TABLE metaschema_public.procedure ( argdefaults text[], lang_name text, definition text, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -394,35 +426,6 @@ COMMENT ON CONSTRAINT db_fkey ON metaschema_public.procedure IS '@omit manyToMan CREATE INDEX procedure_database_id_idx ON metaschema_public.procedure (database_id); -CREATE TABLE metaschema_public.rls_function ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - database_id uuid NOT NULL DEFAULT uuid_nil(), - table_id uuid NOT NULL, - name text, - label text, - description text, - data jsonb, - inline boolean DEFAULT false, - security int DEFAULT 0, - CONSTRAINT db_fkey - FOREIGN KEY(database_id) - REFERENCES metaschema_public.database (id) - ON DELETE CASCADE, - CONSTRAINT table_fkey - FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - UNIQUE (database_id, name) -); - -COMMENT ON CONSTRAINT db_fkey ON metaschema_public.rls_function IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT table_fkey ON metaschema_public.rls_function IS '@omit manyToMany'; - -CREATE INDEX rls_function_table_id_idx ON metaschema_public.rls_function (table_id); - -CREATE INDEX rls_function_database_id_idx ON metaschema_public.rls_function (database_id); - CREATE TABLE metaschema_public.schema_grant ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), database_id uuid NOT NULL DEFAULT uuid_nil(), @@ -501,6 +504,11 @@ CREATE TABLE metaschema_public.trigger ( name text NOT NULL, event text, function_name text, + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -529,6 +537,10 @@ CREATE TABLE metaschema_public.unique_constraint ( smart_tags jsonb, type text, field_ids uuid[] NOT NULL, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/types/object_category.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/types/object_category.sql index 9d5c82f5d..cb6b1b13e 100644 --- a/packages/metaschema-schema/verify/schemas/metaschema_public/types/object_category.sql +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/types/object_category.sql @@ -3,5 +3,7 @@ BEGIN; SELECT 'core'::metaschema_public.object_category; +SELECT 'module'::metaschema_public.object_category; +SELECT 'app'::metaschema_public.object_category; ROLLBACK; diff --git a/packages/services/deploy/schemas/services_public/tables/api_modules/table.sql b/packages/services/deploy/schemas/services_public/tables/api_modules/table.sql index 57cc1b3f9..e95741f97 100644 --- a/packages/services/deploy/schemas/services_public/tables/api_modules/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/api_modules/table.sql @@ -19,6 +19,13 @@ CREATE TABLE services_public.api_modules ( ); +COMMENT ON TABLE services_public.api_modules IS 'Server-side module configuration for an API endpoint; stores module name and JSON settings used by the application server'; +COMMENT ON COLUMN services_public.api_modules.id IS 'Unique identifier for this API module record'; +COMMENT ON COLUMN services_public.api_modules.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.api_modules.api_id IS 'API this module configuration belongs to'; +COMMENT ON COLUMN services_public.api_modules.name IS 'Module name (e.g. auth, uploads, webhooks)'; +COMMENT ON COLUMN services_public.api_modules.data IS 'JSON configuration data for this module'; + ALTER TABLE services_public.api_modules ADD CONSTRAINT api_modules_api_id_fkey FOREIGN KEY ( api_id ) REFERENCES services_public.apis ( id ); COMMENT ON CONSTRAINT api_modules_api_id_fkey ON services_public.api_modules IS E'@omit manyToMany'; CREATE INDEX api_modules_api_id_idx ON services_public.api_modules ( api_id ); diff --git a/packages/services/deploy/schemas/services_public/tables/api_schemas/table.sql b/packages/services/deploy/schemas/services_public/tables/api_schemas/table.sql index 6ed0da289..4530e72a9 100644 --- a/packages/services/deploy/schemas/services_public/tables/api_schemas/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/api_schemas/table.sql @@ -18,6 +18,12 @@ CREATE TABLE services_public.api_schemas ( unique(api_id, schema_id) ); +COMMENT ON TABLE services_public.api_schemas IS 'Join table linking APIs to the database schemas they expose; controls which schemas are accessible through each API'; +COMMENT ON COLUMN services_public.api_schemas.id IS 'Unique identifier for this API-schema mapping'; +COMMENT ON COLUMN services_public.api_schemas.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.api_schemas.schema_id IS 'Metaschema schema being exposed through the API'; +COMMENT ON COLUMN services_public.api_schemas.api_id IS 'API that exposes this schema'; + -- COMMENT ON CONSTRAINT schema_fkey ON services_public.api_schemas IS E'@omit manyToMany'; -- COMMENT ON CONSTRAINT api_fkey ON services_public.api_schemas IS E'@omit manyToMany'; COMMENT ON CONSTRAINT db_fkey ON services_public.api_schemas IS E'@omit manyToMany'; diff --git a/packages/services/deploy/schemas/services_public/tables/apis/table.sql b/packages/services/deploy/schemas/services_public/tables/apis/table.sql index 14d2a8141..b8c907590 100644 --- a/packages/services/deploy/schemas/services_public/tables/apis/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/apis/table.sql @@ -20,6 +20,15 @@ CREATE TABLE services_public.apis ( UNIQUE(database_id, name) ); +COMMENT ON TABLE services_public.apis IS 'API endpoint configurations: each record defines a PostGraphile/PostgREST API with its database role and public access settings'; +COMMENT ON COLUMN services_public.apis.id IS 'Unique identifier for this API'; +COMMENT ON COLUMN services_public.apis.database_id IS 'Reference to the metaschema database this API serves'; +COMMENT ON COLUMN services_public.apis.name IS 'Unique name for this API within its database'; +COMMENT ON COLUMN services_public.apis.dbname IS 'PostgreSQL database name to connect to'; +COMMENT ON COLUMN services_public.apis.role_name IS 'PostgreSQL role used for authenticated requests'; +COMMENT ON COLUMN services_public.apis.anon_role IS 'PostgreSQL role used for anonymous/unauthenticated requests'; +COMMENT ON COLUMN services_public.apis.is_public IS 'Whether this API is publicly accessible without authentication'; + COMMENT ON CONSTRAINT db_fkey ON services_public.apis IS E'@omit manyToMany'; CREATE INDEX apis_database_id_idx ON services_public.apis ( database_id ); diff --git a/packages/services/deploy/schemas/services_public/tables/apps/table.sql b/packages/services/deploy/schemas/services_public/tables/apps/table.sql index 1dbfcc576..1858d10d7 100644 --- a/packages/services/deploy/schemas/services_public/tables/apps/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/apps/table.sql @@ -23,6 +23,17 @@ CREATE TABLE services_public.apps ( UNIQUE ( site_id ) ); +COMMENT ON TABLE services_public.apps IS 'Mobile and native app configuration linked to a site, including store links and identifiers'; +COMMENT ON COLUMN services_public.apps.id IS 'Unique identifier for this app'; +COMMENT ON COLUMN services_public.apps.database_id IS 'Reference to the metaschema database this app belongs to'; +COMMENT ON COLUMN services_public.apps.site_id IS 'Site this app is associated with (one app per site)'; +COMMENT ON COLUMN services_public.apps.name IS 'Display name of the app'; +COMMENT ON COLUMN services_public.apps.app_image IS 'App icon or promotional image'; +COMMENT ON COLUMN services_public.apps.app_store_link IS 'URL to the Apple App Store listing'; +COMMENT ON COLUMN services_public.apps.app_store_id IS 'Apple App Store application identifier'; +COMMENT ON COLUMN services_public.apps.app_id_prefix IS 'Apple App ID prefix (Team ID) for universal links and associated domains'; +COMMENT ON COLUMN services_public.apps.play_store_link IS 'URL to the Google Play Store listing'; + ALTER TABLE services_public.apps ADD CONSTRAINT apps_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); COMMENT ON CONSTRAINT apps_site_id_fkey ON services_public.apps IS E'@omit manyToMany'; CREATE INDEX apps_site_id_idx ON services_public.apps ( site_id ); diff --git a/packages/services/deploy/schemas/services_public/tables/domains/table.sql b/packages/services/deploy/schemas/services_public/tables/domains/table.sql index 708966f78..26aa06f38 100644 --- a/packages/services/deploy/schemas/services_public/tables/domains/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/domains/table.sql @@ -29,6 +29,14 @@ CREATE TABLE services_public.domains ( UNIQUE ( subdomain, domain ) ); +COMMENT ON TABLE services_public.domains IS 'DNS domain and subdomain routing: maps hostnames to either an API endpoint or a site'; +COMMENT ON COLUMN services_public.domains.id IS 'Unique identifier for this domain record'; +COMMENT ON COLUMN services_public.domains.database_id IS 'Reference to the metaschema database this domain belongs to'; +COMMENT ON COLUMN services_public.domains.api_id IS 'API endpoint this domain routes to (mutually exclusive with site_id)'; +COMMENT ON COLUMN services_public.domains.site_id IS 'Site this domain routes to (mutually exclusive with api_id)'; +COMMENT ON COLUMN services_public.domains.subdomain IS 'Subdomain portion of the hostname'; +COMMENT ON COLUMN services_public.domains.domain IS 'Root domain of the hostname'; + COMMENT ON CONSTRAINT db_fkey ON services_public.domains IS E'@omit manyToMany'; CREATE INDEX domains_database_id_idx ON services_public.domains ( database_id ); diff --git a/packages/services/deploy/schemas/services_public/tables/site_metadata/table.sql b/packages/services/deploy/schemas/services_public/tables/site_metadata/table.sql index a90fb92b3..9d6d5afcb 100644 --- a/packages/services/deploy/schemas/services_public/tables/site_metadata/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/site_metadata/table.sql @@ -23,6 +23,14 @@ CREATE TABLE services_public.site_metadata ( ); +COMMENT ON TABLE services_public.site_metadata IS 'SEO and social sharing metadata for a site: page title, description, and Open Graph image'; +COMMENT ON COLUMN services_public.site_metadata.id IS 'Unique identifier for this metadata record'; +COMMENT ON COLUMN services_public.site_metadata.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.site_metadata.site_id IS 'Site this metadata belongs to'; +COMMENT ON COLUMN services_public.site_metadata.title IS 'Page title for SEO (max 120 characters)'; +COMMENT ON COLUMN services_public.site_metadata.description IS 'Meta description for SEO and social sharing (max 120 characters)'; +COMMENT ON COLUMN services_public.site_metadata.og_image IS 'Open Graph image for social media previews'; + ALTER TABLE services_public.site_metadata ADD CONSTRAINT site_metadata_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); COMMENT ON CONSTRAINT site_metadata_site_id_fkey ON services_public.site_metadata IS E'@omit manyToMany'; CREATE INDEX site_metadata_site_id_idx ON services_public.site_metadata ( site_id ); diff --git a/packages/services/deploy/schemas/services_public/tables/site_modules/table.sql b/packages/services/deploy/schemas/services_public/tables/site_modules/table.sql index 565c3aee2..0fc227459 100644 --- a/packages/services/deploy/schemas/services_public/tables/site_modules/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/site_modules/table.sql @@ -17,6 +17,13 @@ CREATE TABLE services_public.site_modules ( CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE ); +COMMENT ON TABLE services_public.site_modules IS 'Site-level module configuration; stores module name and JSON settings used by the frontend or server for each site'; +COMMENT ON COLUMN services_public.site_modules.id IS 'Unique identifier for this site module record'; +COMMENT ON COLUMN services_public.site_modules.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.site_modules.site_id IS 'Site this module configuration belongs to'; +COMMENT ON COLUMN services_public.site_modules.name IS 'Module name (e.g. user_auth_module, analytics)'; +COMMENT ON COLUMN services_public.site_modules.data IS 'JSON configuration data for this module'; + ALTER TABLE services_public.site_modules ADD CONSTRAINT site_modules_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); COMMENT ON CONSTRAINT site_modules_site_id_fkey ON services_public.site_modules IS E'@omit manyToMany'; CREATE INDEX site_modules_site_id_idx ON services_public.site_modules ( site_id ); diff --git a/packages/services/deploy/schemas/services_public/tables/site_themes/table.sql b/packages/services/deploy/schemas/services_public/tables/site_themes/table.sql index 6fe579aec..2c1fd17e2 100644 --- a/packages/services/deploy/schemas/services_public/tables/site_themes/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/site_themes/table.sql @@ -17,6 +17,12 @@ CREATE TABLE services_public.site_themes ( CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE ); +COMMENT ON TABLE services_public.site_themes IS 'Theme configuration for a site; stores design tokens, colors, and typography as JSONB'; +COMMENT ON COLUMN services_public.site_themes.id IS 'Unique identifier for this theme record'; +COMMENT ON COLUMN services_public.site_themes.database_id IS 'Reference to the metaschema database'; +COMMENT ON COLUMN services_public.site_themes.site_id IS 'Site this theme belongs to'; +COMMENT ON COLUMN services_public.site_themes.theme IS 'JSONB object containing theme tokens (colors, typography, spacing, etc.)'; + ALTER TABLE services_public.site_themes ADD CONSTRAINT site_themes_site_id_fkey FOREIGN KEY ( site_id ) REFERENCES services_public.sites ( id ); COMMENT ON CONSTRAINT site_themes_site_id_fkey ON services_public.site_themes IS E'@omit manyToMany'; CREATE INDEX site_themes_site_id_idx ON services_public.site_themes ( site_id ); diff --git a/packages/services/deploy/schemas/services_public/tables/sites/table.sql b/packages/services/deploy/schemas/services_public/tables/sites/table.sql index beb95d949..c9bccfc82 100644 --- a/packages/services/deploy/schemas/services_public/tables/sites/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/sites/table.sql @@ -24,6 +24,17 @@ CREATE TABLE services_public.sites ( CONSTRAINT max_descr CHECK ( character_length(description) <= 120 ) ); +COMMENT ON TABLE services_public.sites IS 'Top-level site configuration: branding assets, title, and description for a deployed application'; +COMMENT ON COLUMN services_public.sites.id IS 'Unique identifier for this site'; +COMMENT ON COLUMN services_public.sites.database_id IS 'Reference to the metaschema database this site belongs to'; +COMMENT ON COLUMN services_public.sites.title IS 'Display title for the site (max 120 characters)'; +COMMENT ON COLUMN services_public.sites.description IS 'Short description of the site (max 120 characters)'; +COMMENT ON COLUMN services_public.sites.og_image IS 'Open Graph image used for social media link previews'; +COMMENT ON COLUMN services_public.sites.favicon IS 'Browser favicon attachment'; +COMMENT ON COLUMN services_public.sites.apple_touch_icon IS 'Apple touch icon for iOS home screen bookmarks'; +COMMENT ON COLUMN services_public.sites.logo IS 'Primary logo image for the site'; +COMMENT ON COLUMN services_public.sites.dbname IS 'PostgreSQL database name this site connects to'; + COMMENT ON CONSTRAINT db_fkey ON services_public.sites IS E'@omit manyToMany'; CREATE INDEX sites_database_id_idx ON services_public.sites ( database_id ); diff --git a/packages/types/Makefile b/packages/types/Makefile index 7141b96dd..d59535666 100644 --- a/packages/types/Makefile +++ b/packages/types/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-types -DATA = sql/pgpm-types--0.16.0.sql +DATA = sql/pgpm-types--0.15.5.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/types/__tests__/domains.pgutils.test.ts b/packages/types/__tests__/domains.pgutils.test.ts index f2e5c8156..7e01d98f1 100644 --- a/packages/types/__tests__/domains.pgutils.test.ts +++ b/packages/types/__tests__/domains.pgutils.test.ts @@ -1,76 +1,78 @@ import { getConnections, PgTestClient } from 'pgsql-test'; -// Validation rules: -// - url: lenient regex ^https?://[^\s]+$ (must start with http/https, no whitespace, paths allowed) -// - attachment: lenient regex ^https?://[^\s]+$ (same as url) -// - hostname: no whitespace (^[^\s]+$) -// - email: must contain @ (value ~ '@') -// - image: jsonb object requiring 'url' OR 'id' OR 'key', with type validation, optional bucket/provider/mime/versions (versions is array) -// - upload: jsonb object requiring 'url' OR 'id' OR 'key', with type validation on all fields, optional bucket/provider/mime - const validUrls = [ 'http://foo.com/blah_blah', 'http://foo.com/blah_blah/', 'http://foo.com/blah_blah_(wikipedia)', + 'http://foo.com/blah_blah_(wikipedia)_(again)', 'http://www.example.com/wpstyle/?p=364', 'https://www.example.com/foo/?bar=baz&inga=42&quux', + 'http://✪df.ws/123', 'http://foo.com/blah_(wikipedia)#cite-1', + 'http://foo.com/blah_(wikipedia)_blah#cite-1', 'http://foo.com/(something)?after=parens', 'http://code.google.com/events/#&product=browser', 'http://j.mp', 'http://foo.bar/?q=Test%20URL-encoded%20stuff', + 'http://مثال.إختبار', + 'http://例子.测试', + 'http://उदाहरण.परीक्षा', + "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", 'http://1337.net', 'http://a.b-c.de', 'https://foo_bar.example.com/' ]; const invalidUrls = [ + 'http://##', + 'http://##/', + 'http://foo.bar?q=Spaces should be encoded', + '//', + '//a', + '///a', + '///', + 'http:///a', 'foo.com', - 'ftp://foo.bar/', - 'not-a-url', - 'random text with spaces', - '//missing-protocol.com' + 'rdar://1234', + 'h://test', + 'http:// shouldfail.com', + ':// should fail', + 'http://foo.bar/foo(bar)baz quux', + 'ftps://foo.bar/', + 'http://.www.foo.bar/', + 'http://.www.foo.bar./' +]; + +const validAttachments = [ + 'http://www.foo.bar/some.jpg', + 'https://foo.bar/some.PNG' +]; + +const invalidAttachments = [ + 'hi there', + 'ftp://foo.bar/some.png', + 'https:///foo.bar/some.png' ]; const validImages = [ - { url: 'http://www.foo.bar/some.jpg' }, - { url: 'https://foo.bar/some.PNG' }, - { url: 'https://example.com/path/to/image.png' }, - { url: 'https://example.com/image.png', bucket: 'my-bucket' }, - { url: 'https://example.com/image.png', provider: 's3', mime: 'image/png' }, - { id: 'some-image-id' }, - { key: 'some-image-key' }, - { id: 'private-image', bucket: 'my-bucket', provider: 's3' }, - { url: 'https://example.com/image.png', versions: ['thumb', 'large'] } + { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, + { url: 'https://foo.bar/some.PNG', mime: 'image/jpg' } ]; const invalidImages = [ - { notUrl: 'missing required keys' }, - { mime: 'only mime, no url/id/key' }, - { url: 'not-a-valid-url' }, - { url: 'ftp://wrong-protocol.com/image.png' }, - { id: 123 }, - { key: true }, - { url: 'https://example.com/image.png', bucket: 123 }, - { url: 'https://example.com/image.png', versions: 'not-an-array' } + { url: 'hi there' }, + { url: 'https://foo.bar/some.png' } ]; const validUploads = [ - { url: 'http://www.foo.bar/some.jpg' }, - { url: 'https://foo.bar/some.PNG' }, - { id: 'some-id' }, - { key: 'some-key' }, - { url: 'https://example.com/file.pdf', id: 'with-id' }, - { id: 'some-id', bucket: 'my-bucket', provider: 's3', mime: 'application/pdf' } + { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, + { url: 'https://foo.bar/some.PNG', mime: 'image/png' } ]; const invalidUploads = [ - { notUrl: 'missing required keys' }, - { mime: 'only mime, no url/id/key' }, - { url: 'not-a-valid-url' }, - { url: 'ftp://wrong-protocol.com/file.pdf' }, - { id: 123 }, - { key: true } + { url: 'hi there' }, + { url: 'https://foo.bar/some.png' }, + { url: 'ftp://foo.bar/some.png', mime: 'image/png' } ]; let pg: PgTestClient; @@ -107,152 +109,129 @@ afterAll(async () => { }); describe('types', () => { - describe('url domain (lenient regex: ^https?://[^\\s]+$)', () => { - it('accepts valid URLs', async () => { - for (const value of validUrls) { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } - }); - - it('rejects invalid URLs', async () => { - for (const value of invalidUrls) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('valid attachment and image', async () => { + for (const attachment of validAttachments) { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); + } + + for (const image of validImages) { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } }); - describe('hostname domain (no whitespace: ^[^\\s]+$)', () => { - it('accepts values without whitespace', async () => { - const values = [ - 'google.com', - 'www.example.com', - 'not-a-hostname', - 'http://with-protocol.com' - ]; - for (const value of values) { - await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); + it('invalid attachment and image', async () => { + for (const attachment of invalidAttachments) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } - it('rejects values with whitespace', async () => { - const invalidValues = [ - 'has spaces', - 'has\ttab' - ]; - for (const value of invalidValues) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + for (const image of invalidImages) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } }); - describe('attachment domain (lenient regex: ^https?://[^\\s]+$)', () => { - it('accepts valid URLs', async () => { - const values = [ - 'http://www.foo.bar/some.jpg', - 'https://foo.bar/some.PNG', - 'https://example.com/path/to/file.pdf' - ]; - for (const value of values) { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); - } - }); + it('valid upload', async () => { + for (const upload of validUploads) { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } + }); - it('rejects invalid URLs', async () => { - const invalidValues = [ - 'not-a-url', - 'ftp://wrong-protocol.com/file.pdf', - 'random text with spaces' - ]; - for (const value of invalidValues) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + it('invalid upload', async () => { + for (const upload of invalidUploads) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } }); - describe('email domain (must contain @)', () => { - it('accepts values containing @', async () => { - const values = [ - 'd@google.com', - 'user@example.org', - 'test@localhost' - ]; - for (const value of values) { - await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); - } - }); + it('valid url', async () => { + for (const value of validUrls) { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); + } + }); - it('rejects values without @', async () => { - const invalidValues = [ - 'not-an-email', - 'missing.at.sign' - ]; - for (const value of invalidValues) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + it('invalid url', async () => { + for (const value of invalidUrls) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } }); - describe('image domain (jsonb requiring url OR id OR key, optional versions array)', () => { - it('accepts valid images with url, id, or key', async () => { - for (const image of validImages) { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } - }); + it('email', async () => { + await pg.any(` + INSERT INTO customers (email) VALUES + ('d@google.com'), + ('d@google.in'), + ('d@google.in'), + ('d@www.google.in'), + ('d@google.io'), + ('dan@google.some.other.com')`); + }); - it('rejects invalid images', async () => { - for (const image of invalidImages) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('not email', async () => { + let failed = false; + try { + await pg.any(` + INSERT INTO customers (email) VALUES + ('http://google.some.other.com')`); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); }); - describe('upload domain (jsonb requiring url OR id OR key)', () => { - it('accepts valid uploads with url, id, or key', async () => { - for (const upload of validUploads) { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } - }); + it('hostname', async () => { + await pg.any(` + INSERT INTO customers (domain) VALUES + ('google.com'), + ('google.in'), + ('google.in'), + ('www.google.in'), + ('google.io'), + ('google.some.other.com')`); + }); - it('rejects uploads without url, id, or key', async () => { - for (const upload of invalidUploads) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('not hostname', async () => { + let failed = false; + try { + await pg.any(` + INSERT INTO customers (domain) VALUES + ('http://google.some.other.com')`); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + }); + + it('not hostname 2', async () => { + let failed = false; + try { + await pg.any(` + INSERT INTO customers (domain) VALUES + ('google.some.other.com/a/b/d')`); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); }); }); diff --git a/packages/types/__tests__/domains.test.ts b/packages/types/__tests__/domains.test.ts index 6594f302e..7e01d98f1 100644 --- a/packages/types/__tests__/domains.test.ts +++ b/packages/types/__tests__/domains.test.ts @@ -1,113 +1,78 @@ import { getConnections, PgTestClient } from 'pgsql-test'; -// Validation rules: -// - url: lenient regex ^https?://[^\s]+$ (must start with http/https, no whitespace, paths allowed) -// - origin: strict regex ^https?://[^/\s]+$ (protocol + host only, no paths for CORS security) -// - attachment: lenient regex ^https?://[^\s]+$ (same as url) -// - hostname: no whitespace (^[^\s]+$) -// - email: must contain @ (value ~ '@') -// - image: jsonb object requiring 'url' OR 'id' OR 'key', with type validation, optional bucket/provider/mime/versions (versions is array) -// - upload: jsonb object requiring 'url' OR 'id' OR 'key', with type validation on all fields, optional bucket/provider/mime - const validUrls = [ 'http://foo.com/blah_blah', 'http://foo.com/blah_blah/', 'http://foo.com/blah_blah_(wikipedia)', + 'http://foo.com/blah_blah_(wikipedia)_(again)', 'http://www.example.com/wpstyle/?p=364', 'https://www.example.com/foo/?bar=baz&inga=42&quux', + 'http://✪df.ws/123', 'http://foo.com/blah_(wikipedia)#cite-1', + 'http://foo.com/blah_(wikipedia)_blah#cite-1', 'http://foo.com/(something)?after=parens', 'http://code.google.com/events/#&product=browser', 'http://j.mp', 'http://foo.bar/?q=Test%20URL-encoded%20stuff', + 'http://مثال.إختبار', + 'http://例子.测试', + 'http://उदाहरण.परीक्षा', + "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", 'http://1337.net', 'http://a.b-c.de', 'https://foo_bar.example.com/' ]; const invalidUrls = [ + 'http://##', + 'http://##/', + 'http://foo.bar?q=Spaces should be encoded', + '//', + '//a', + '///a', + '///', + 'http:///a', 'foo.com', - 'ftp://foo.bar/', - 'not-a-url', - 'random text with spaces', - '//missing-protocol.com' + 'rdar://1234', + 'h://test', + 'http:// shouldfail.com', + ':// should fail', + 'http://foo.bar/foo(bar)baz quux', + 'ftps://foo.bar/', + 'http://.www.foo.bar/', + 'http://.www.foo.bar./' ]; -// Valid origins: protocol + host only (no paths) -const validOrigins = [ - 'http://example.com', - 'https://example.com', - 'http://localhost:3000', - 'https://api.example.com:8080', - 'http://192.168.1.1', - 'https://foo_bar.example.com' +const validAttachments = [ + 'http://www.foo.bar/some.jpg', + 'https://foo.bar/some.PNG' ]; -// Invalid origins: paths, query strings, fragments, or non-http protocols -const invalidOrigins = [ - 'https://example.com/', - 'https://example.com/path', - 'https://example.com/malicious/path', - 'https://example.com?query=1', - 'https://example.com#fragment', - 'ftp://example.com', - 'foo.com', - 'not-an-origin' +const invalidAttachments = [ + 'hi there', + 'ftp://foo.bar/some.png', + 'https:///foo.bar/some.png' ]; const validImages = [ - { url: 'http://www.foo.bar/some.jpg' }, - { url: 'https://foo.bar/some.PNG' }, - { url: 'https://example.com/path/to/image.png' }, - { url: 'https://example.com/image.png', bucket: 'my-bucket' }, - { url: 'https://example.com/image.png', provider: 's3' }, - { url: 'https://example.com/image.png', mime: 'image/png' }, - { url: 'https://example.com/image.png', bucket: 'my-bucket', provider: 's3', mime: 'image/jpeg' }, - { id: 'some-image-id' }, - { key: 'some-image-key' }, - { id: 'private-image', bucket: 'my-bucket', provider: 's3' }, - { url: 'https://example.com/image.png', versions: ['thumb', 'medium', 'large'] }, - { id: 'image-with-versions', versions: [{ size: 'thumb' }, { size: 'large' }] } + { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, + { url: 'https://foo.bar/some.PNG', mime: 'image/jpg' } ]; const invalidImages = [ - { notUrl: 'missing required keys' }, - { mime: 'only mime, no url/id/key' }, - { url: 'not-a-valid-url' }, - { url: 'ftp://wrong-protocol.com/image.png' }, - { id: 123 }, - { key: true }, - { url: 'https://example.com/image.png', bucket: 123 }, - { url: 'https://example.com/image.png', provider: true }, - { url: 'https://example.com/image.png', mime: ['array'] }, - { url: 'https://example.com/image.png', versions: 'not-an-array' }, - 'not-an-object', - ['array-not-object'] + { url: 'hi there' }, + { url: 'https://foo.bar/some.png' } ]; const validUploads = [ - { url: 'http://www.foo.bar/some.jpg' }, - { url: 'https://foo.bar/some.PNG' }, - { id: 'some-id' }, - { key: 'some-key' }, - { url: 'https://example.com/file.pdf', id: 'with-id' }, - { id: 'some-id', bucket: 'my-bucket', provider: 's3' }, - { key: 'some-key', mime: 'application/pdf' }, - { url: 'https://example.com/file.pdf', bucket: 'bucket', provider: 'gcs', mime: 'application/pdf' } + { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, + { url: 'https://foo.bar/some.PNG', mime: 'image/png' } ]; const invalidUploads = [ - { notUrl: 'missing required keys' }, - { mime: 'only mime, no url/id/key' }, - { url: 'not-a-valid-url' }, - { url: 'ftp://wrong-protocol.com/file.pdf' }, - { id: 123 }, - { key: true }, - { url: 'https://example.com/file.pdf', bucket: 123 }, - { url: 'https://example.com/file.pdf', provider: ['array'] }, - { id: 'some-id', mime: { nested: 'object' } }, - 'not-an-object', - ['array-not-object'] + { url: 'hi there' }, + { url: 'https://foo.bar/some.png' }, + { url: 'ftp://foo.bar/some.png', mime: 'image/png' } ]; let pg: PgTestClient; @@ -122,7 +87,6 @@ beforeAll(async () => { CREATE TABLE customers ( id serial, url url, - origin origin, image image, attachment attachment, domain hostname, @@ -145,176 +109,129 @@ afterAll(async () => { }); describe('types', () => { - describe('url domain (lenient regex: ^https?://[^\\s]+$)', () => { - it('accepts valid URLs', async () => { - for (const value of validUrls) { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } - }); - - it('rejects invalid URLs', async () => { - for (const value of invalidUrls) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('valid attachment and image', async () => { + for (const attachment of validAttachments) { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); + } + + for (const image of validImages) { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } }); - describe('origin domain (strict regex: ^https?://[^/\\s]+$ - no paths)', () => { - it('accepts valid origins (protocol + host only)', async () => { - for (const value of validOrigins) { - await pg.any(`INSERT INTO customers (origin) VALUES ($1);`, [value]); + it('invalid attachment and image', async () => { + for (const attachment of invalidAttachments) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } - it('rejects origins with paths, query strings, or invalid protocols', async () => { - for (const value of invalidOrigins) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (origin) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + for (const image of invalidImages) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } }); - describe('hostname domain (no whitespace: ^[^\\s]+$)', () => { - it('accepts values without whitespace', async () => { - const values = [ - 'google.com', - 'www.example.com', - 'not-a-hostname', - 'http://with-protocol.com', - 'anything-without-spaces' - ]; - for (const value of values) { - await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); - } - }); - - it('rejects values with whitespace', async () => { - const invalidValues = [ - 'has spaces', - 'has\ttab', - 'has\nnewline' - ]; - for (const value of invalidValues) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('valid upload', async () => { + for (const upload of validUploads) { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } }); - describe('attachment domain (lenient regex: ^https?://[^\\s]+$)', () => { - it('accepts valid URLs', async () => { - const values = [ - 'http://www.foo.bar/some.jpg', - 'https://foo.bar/some.PNG', - 'https://example.com/path/to/file.pdf' - ]; - for (const value of values) { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); + it('invalid upload', async () => { + for (const upload of invalidUploads) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } + }); - it('rejects invalid URLs', async () => { - const invalidValues = [ - 'not-a-url', - 'ftp://wrong-protocol.com/file.pdf', - 'random text with spaces' - ]; - for (const value of invalidValues) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('valid url', async () => { + for (const value of validUrls) { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); + } }); - describe('email domain (must contain @)', () => { - it('accepts values containing @', async () => { - const values = [ - 'd@google.com', - 'user@example.org', - 'test@localhost', - 'weird@but@valid' - ]; - for (const value of values) { - await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); + it('invalid url', async () => { + for (const value of invalidUrls) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); + } catch (e) { + failed = true; } - }); + expect(failed).toBe(true); + } + }); - it('rejects values without @', async () => { - const invalidValues = [ - 'not-an-email', - 'random text', - 'missing.at.sign' - ]; - for (const value of invalidValues) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('email', async () => { + await pg.any(` + INSERT INTO customers (email) VALUES + ('d@google.com'), + ('d@google.in'), + ('d@google.in'), + ('d@www.google.in'), + ('d@google.io'), + ('dan@google.some.other.com')`); }); - describe('image domain (jsonb requiring url OR id OR key, optional versions array)', () => { - it('accepts valid images with url, id, or key', async () => { - for (const image of validImages) { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } - }); + it('not email', async () => { + let failed = false; + try { + await pg.any(` + INSERT INTO customers (email) VALUES + ('http://google.some.other.com')`); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + }); - it('rejects invalid images', async () => { - for (const image of invalidImages) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('hostname', async () => { + await pg.any(` + INSERT INTO customers (domain) VALUES + ('google.com'), + ('google.in'), + ('google.in'), + ('www.google.in'), + ('google.io'), + ('google.some.other.com')`); }); - describe('upload domain (jsonb requiring url OR id OR key)', () => { - it('accepts valid uploads with url, id, or key', async () => { - for (const upload of validUploads) { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } - }); + it('not hostname', async () => { + let failed = false; + try { + await pg.any(` + INSERT INTO customers (domain) VALUES + ('http://google.some.other.com')`); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + }); - it('rejects uploads without url, id, or key', async () => { - for (const upload of invalidUploads) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); - } - }); + it('not hostname 2', async () => { + let failed = false; + try { + await pg.any(` + INSERT INTO customers (domain) VALUES + ('google.some.other.com/a/b/d')`); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); }); }); diff --git a/packages/types/pgpm-types.control b/packages/types/pgpm-types.control index ce373f4f4..7c098cbe3 100644 --- a/packages/types/pgpm-types.control +++ b/packages/types/pgpm-types.control @@ -1,6 +1,6 @@ # pgpm-types extension comment = 'pgpm-types extension' -default_version = '0.16.0' +default_version = '0.15.5' module_pathname = '$libdir/pgpm-types' requires = 'plpgsql,citext,pgpm-verify' relocatable = false diff --git a/packages/types/sql/pgpm-types--0.15.5.sql b/packages/types/sql/pgpm-types--0.15.5.sql new file mode 100644 index 000000000..76d524a4d --- /dev/null +++ b/packages/types/sql/pgpm-types--0.15.5.sql @@ -0,0 +1,71 @@ +\echo Use "CREATE EXTENSION pgpm-types" to load this file. \quit +CREATE DOMAIN attachment AS text + CHECK (value ~ E'^https?://[^\\s]+$'); + +COMMENT ON DOMAIN attachment IS '@name constructiveInternalTypeAttachment'; + +CREATE DOMAIN email AS citext + CHECK (value ~ '@'); + +COMMENT ON DOMAIN email IS '@name constructiveInternalTypeEmail'; + +CREATE DOMAIN hostname AS text + CHECK (value ~ E'^[^\\s]+$'); + +COMMENT ON DOMAIN hostname IS '@name constructiveInternalTypeHostname'; + +CREATE DOMAIN image AS jsonb + CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' + OR value ? 'id' + OR value ? 'key') + AND (NOT (value ? 'url') + OR (value ->> 'url') ~ E'^https?://[^\\s]+$') + AND (NOT (value ? 'id') + OR jsonb_typeof(value -> 'id') = 'string') + AND (NOT (value ? 'key') + OR jsonb_typeof(value -> 'key') = 'string') + AND (NOT (value ? 'bucket') + OR jsonb_typeof(value -> 'bucket') = 'string') + AND (NOT (value ? 'provider') + OR jsonb_typeof(value -> 'provider') = 'string') + AND (NOT (value ? 'mime') + OR jsonb_typeof(value -> 'mime') = 'string') + AND (NOT (value ? 'versions') + OR jsonb_typeof(value -> 'versions') = 'array') +); + +COMMENT ON DOMAIN image IS '@name constructiveInternalTypeImage'; + +CREATE DOMAIN origin AS text + CHECK (value ~ E'^https?://[^/\\s]+$'); + +COMMENT ON DOMAIN origin IS '@name constructiveInternalTypeOrigin'; + +CREATE DOMAIN upload AS jsonb + CHECK ( + jsonb_typeof(value) = 'object' + AND (value ? 'url' + OR value ? 'id' + OR value ? 'key') + AND (NOT (value ? 'url') + OR (value ->> 'url') ~ E'^https?://[^\\s]+$') + AND (NOT (value ? 'id') + OR jsonb_typeof(value -> 'id') = 'string') + AND (NOT (value ? 'key') + OR jsonb_typeof(value -> 'key') = 'string') + AND (NOT (value ? 'bucket') + OR jsonb_typeof(value -> 'bucket') = 'string') + AND (NOT (value ? 'provider') + OR jsonb_typeof(value -> 'provider') = 'string') + AND (NOT (value ? 'mime') + OR jsonb_typeof(value -> 'mime') = 'string') +); + +COMMENT ON DOMAIN upload IS '@name constructiveInternalTypeUpload'; + +CREATE DOMAIN url AS text + CHECK (value ~ E'^https?://[^\\s]+$'); + +COMMENT ON DOMAIN url IS '@name constructiveInternalTypeUrl'; \ No newline at end of file From 4b2748ce1d524f807bd5258eafbd5c26e077e601 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Feb 2026 11:24:54 +0000 Subject: [PATCH 2/3] fix: revert incompatible test changes, update table_module assertions, delete stale snapshots - Revert types test rewrites (domain validation tests written for stricter constraints) - Revert types Makefile/control version downgrade (keep 0.16.0) - Revert metaschema-schema test rewrite (depends on services module not available in isolated CI) - Update table_module test: private_schema_id -> schema_id, add table_name and use_rls assertions - Delete stale snapshot files for metaschema-modules, metaschema-schema, and services (will regenerate) --- .../__snapshots__/modules.test.ts.snap | 473 ------------------ .../__tests__/modules.test.ts | 6 +- .../__tests__/__snapshots__/meta.test.ts.snap | 147 ------ .../metaschema-schema/__tests__/meta.test.ts | 195 +------- .../__snapshots__/services.test.ts.snap | 147 ------ packages/types/Makefile | 2 +- .../types/__tests__/domains.pgutils.test.ts | 317 ++++++------ packages/types/__tests__/domains.test.ts | 367 ++++++++------ packages/types/pgpm-types.control | 2 +- 9 files changed, 402 insertions(+), 1254 deletions(-) delete mode 100644 packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap delete mode 100644 packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap delete mode 100644 packages/services/__tests__/__snapshots__/services.test.ts.snap diff --git a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap deleted file mode 100644 index 459a5bc63..000000000 --- a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap +++ /dev/null @@ -1,473 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`db_meta_modules should have all expected module tables 1`] = ` -{ - "moduleNames": [ - "connected_accounts_module", - "crypto_addresses_module", - "crypto_auth_module", - "default_ids_module", - "emails_module", - "encrypted_secrets_module", - "field_module", - "hierarchy_module", - "invites_module", - "levels_module", - "limits_module", - "membership_types_module", - "memberships_module", - "permissions_module", - "phone_numbers_module", - "profiles_module", - "rls_module", - "secrets_module", - "sessions_module", - "table_module", - "table_template_module", - "user_auth_module", - "users_module", - "uuid_module", - ], -} -`; - -exports[`db_meta_modules should verify all module tables exist in metaschema_modules_public schema 1`] = ` -{ - "moduleTablesCount": 24, - "totalTables": 26, -} -`; - -exports[`db_meta_modules should verify emails_module table structure 1`] = ` -{ - "columns": [ - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "database_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "private_schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "owner_table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "table_name", - "data_type": "text", - "is_nullable": "NO", - }, - ], -} -`; - -exports[`db_meta_modules should verify field_module table structure 1`] = ` -{ - "columns": [ - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "database_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "private_schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "field_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "node_type", - "data_type": "text", - "is_nullable": "NO", - }, - { - "column_default": "'{}'::jsonb", - "column_name": "data", - "data_type": "jsonb", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "triggers", - "data_type": "ARRAY", - "is_nullable": "YES", - }, - { - "column_default": null, - "column_name": "functions", - "data_type": "ARRAY", - "is_nullable": "YES", - }, - ], -} -`; - -exports[`db_meta_modules should verify module table structures have database_id foreign keys 1`] = ` -{ - "constraintCount": 72600, -} -`; - -exports[`db_meta_modules should verify module tables have proper foreign key relationships 1`] = ` -{ - "constraintCount": 102535, - "foreignTables": [ - "apis", - "database", - "field", - "schema", - "table", - ], -} -`; - -exports[`db_meta_modules should verify specific module table column defaults 1`] = ` -{ - "sessionsDefaults": [ - { - "column_default": "'app_auth_settings'::text", - "column_name": "auth_settings_table", - }, - { - "column_default": "uuid_nil()", - "column_name": "auth_settings_table_id", - }, - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - }, - { - "column_default": "uuid_nil()", - "column_name": "schema_id", - }, - { - "column_default": "'session_credentials'::text", - "column_name": "session_credentials_table", - }, - { - "column_default": "uuid_nil()", - "column_name": "session_credentials_table_id", - }, - { - "column_default": "'30 days'::interval", - "column_name": "sessions_default_expiration", - }, - { - "column_default": "'sessions'::text", - "column_name": "sessions_table", - }, - { - "column_default": "uuid_nil()", - "column_name": "sessions_table_id", - }, - { - "column_default": "uuid_nil()", - "column_name": "users_table_id", - }, - ], - "usersDefaults": [ - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - }, - { - "column_default": "uuid_nil()", - "column_name": "schema_id", - }, - { - "column_default": "uuid_nil()", - "column_name": "table_id", - }, - { - "column_default": "'users'::text", - "column_name": "table_name", - }, - { - "column_default": "uuid_nil()", - "column_name": "type_table_id", - }, - { - "column_default": "'role_types'::text", - "column_name": "type_table_name", - }, - ], -} -`; - -exports[`db_meta_modules should verify table_module table structure 1`] = ` -{ - "columns": [ - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "database_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "private_schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "node_type", - "data_type": "text", - "is_nullable": "NO", - }, - { - "column_default": "'{}'::jsonb", - "column_name": "data", - "data_type": "jsonb", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "fields", - "data_type": "ARRAY", - "is_nullable": "YES", - }, - ], -} -`; - -exports[`db_meta_modules should verify table_template_module table structure 1`] = ` -{ - "columns": [ - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "database_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "private_schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "owner_table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "table_name", - "data_type": "text", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "node_type", - "data_type": "text", - "is_nullable": "NO", - }, - { - "column_default": "'{}'::jsonb", - "column_name": "data", - "data_type": "jsonb", - "is_nullable": "NO", - }, - ], -} -`; - -exports[`db_meta_modules should verify sessions_module table structure 1`] = ` -{ - "columns": [ - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "database_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "sessions_table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "session_credentials_table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "auth_settings_table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "users_table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "'30 days'::interval", - "column_name": "sessions_default_expiration", - "data_type": "interval", - "is_nullable": "NO", - }, - { - "column_default": "'sessions'::text", - "column_name": "sessions_table", - "data_type": "text", - "is_nullable": "NO", - }, - { - "column_default": "'session_credentials'::text", - "column_name": "session_credentials_table", - "data_type": "text", - "is_nullable": "NO", - }, - { - "column_default": "'app_auth_settings'::text", - "column_name": "auth_settings_table", - "data_type": "text", - "is_nullable": "NO", - }, - ], -} -`; - -exports[`db_meta_modules should verify users_module table structure 1`] = ` -{ - "columns": [ - { - "column_default": "uuid_generate_v4()", - "column_name": "id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": null, - "column_name": "database_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "schema_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "'users'::text", - "column_name": "table_name", - "data_type": "text", - "is_nullable": "NO", - }, - { - "column_default": "uuid_nil()", - "column_name": "type_table_id", - "data_type": "uuid", - "is_nullable": "NO", - }, - { - "column_default": "'role_types'::text", - "column_name": "type_table_name", - "data_type": "text", - "is_nullable": "NO", - }, - ], -} -`; diff --git a/packages/metaschema-modules/__tests__/modules.test.ts b/packages/metaschema-modules/__tests__/modules.test.ts index 26720406b..9fa747d16 100644 --- a/packages/metaschema-modules/__tests__/modules.test.ts +++ b/packages/metaschema-modules/__tests__/modules.test.ts @@ -302,9 +302,11 @@ describe('db_meta_modules', () => { const columnNames = columns.map(c => c.column_name); expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); - expect(columnNames).toContain('private_schema_id'); + expect(columnNames).toContain('schema_id'); expect(columnNames).toContain('table_id'); + expect(columnNames).toContain('table_name'); expect(columnNames).toContain('node_type'); + expect(columnNames).toContain('use_rls'); expect(columnNames).toContain('data'); expect(columnNames).toContain('fields'); @@ -339,4 +341,4 @@ describe('db_meta_modules', () => { expect(snapshot({ columns })).toMatchSnapshot(); }); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap b/packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap deleted file mode 100644 index fbb35cce5..000000000 --- a/packages/metaschema-schema/__tests__/__snapshots__/meta.test.ts.snap +++ /dev/null @@ -1,147 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`db_meta functionality should handle complete meta workflow 1`] = ` -{ - "hash": null, - "id": "[ID]", - "label": null, - "name": "my-meta-db", - "owner_id": "[ID]", - "private_schema_name": null, - "schema_hash": null, - "schema_name": null, -} -`; - -exports[`db_meta functionality should handle complete meta workflow 2`] = ` -{ - "anon_role": "anonymous", - "database_id": "[ID]", - "dbname": "test-database", - "id": "[ID]", - "is_public": true, - "name": "public", - "role_name": "authenticated", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 3`] = ` -{ - "anon_role": "administrator", - "database_id": "[ID]", - "dbname": "test-database", - "id": "[ID]", - "is_public": true, - "name": "admin", - "role_name": "administrator", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 4`] = ` -{ - "apple_touch_icon": null, - "database_id": "[ID]", - "dbname": "test-database", - "description": "Website Description", - "favicon": null, - "id": "[ID]", - "logo": null, - "og_image": null, - "title": "Website Title", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 5`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "domain": "pgpm.io", - "id": "[ID]", - "site_id": null, - "subdomain": "api", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 6`] = ` -{ - "api_id": null, - "database_id": "[ID]", - "domain": "pgpm.io", - "id": "[ID]", - "site_id": "[ID]", - "subdomain": "app", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 7`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "domain": "pgpm.io", - "id": "[ID]", - "site_id": null, - "subdomain": "admin", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 8`] = ` -{ - "data": { - "supportEmail": "support@interweb.co", - }, - "database_id": "[ID]", - "id": "[ID]", - "name": "legal-emails", - "site_id": "[ID]", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 9`] = ` -{ - "api_id": "[ID]", - "data": { - "authenticate": "authenticate", - "authenticate_schema": "services_private", - }, - "database_id": "[ID]", - "id": "[ID]", - "name": "rls_module", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 10`] = ` -{ - "data": { - "auth_schema": "services_public", - "forgot_password": "forgot_password", - "reset_password": "reset_password", - "send_verification_email": "send_verification_email", - "set_password": "set_password", - "sign_in": "login", - "sign_up": "register", - "verify_email": "verify_email", - }, - "database_id": "[ID]", - "id": "[ID]", - "name": "user_auth_module", - "site_id": "[ID]", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 11`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "id": "[ID]", - "schema_id": "[ID]", -} -`; - -exports[`db_meta functionality should handle complete meta workflow 12`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "id": "[ID]", - "schema_id": "[ID]", -} -`; diff --git a/packages/metaschema-schema/__tests__/meta.test.ts b/packages/metaschema-schema/__tests__/meta.test.ts index fe622fa33..51614e20c 100644 --- a/packages/metaschema-schema/__tests__/meta.test.ts +++ b/packages/metaschema-schema/__tests__/meta.test.ts @@ -1,9 +1,9 @@ -import { getConnections, PgTestClient, snapshot } from 'pgsql-test'; +import { getConnections, PgTestClient } from 'pgsql-test'; let pg: PgTestClient; let teardown: () => Promise; -describe('db_meta functionality', () => { +describe('metaschema_schema functionality', () => { beforeAll(async () => { ({ pg, teardown } = await getConnections()); }); @@ -14,7 +14,6 @@ describe('db_meta functionality', () => { beforeEach(async () => { await pg.beforeEach(); - // Grant execute permissions for functions await pg.any(`GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA public TO public`); }); @@ -22,172 +21,6 @@ describe('db_meta functionality', () => { await pg.afterEach(); }); - it('should handle complete meta workflow', async () => { - const objs: Record = { - tables: {}, - domains: {}, - apis: {}, - sites: {} - }; - - const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; - - // Helper function for snapshots - const snap = (obj: any) => { - expect(snapshot(obj)).toMatchSnapshot(); - }; - - // Helper function for snapshots with dbname normalization - const snapWithNormalizedDbname = (obj: any) => { - const normalized = { - ...obj, - dbname: 'test-database' // Replace dynamic dbname with static value - }; - expect(snapshot(normalized)).toMatchSnapshot(); - }; - - // Step 1: Create database - const [database] = await pg.any( - `INSERT INTO metaschema_public.database (owner_id, name) - VALUES ($1, $2) - RETURNING *`, - [owner_id, 'my-meta-db'] - ); - objs.db = database; - const database_id = database.id; - expect(snapshot(database)).toMatchSnapshot(); - - // Step 2: Create APIs first (since domains reference them) - const [publicApi] = await pg.any( - `INSERT INTO services_public.apis (database_id, name, role_name, anon_role) - VALUES ($1, $2, $3, $4) - RETURNING *`, - [database_id, 'public', 'authenticated', 'anonymous'] - ); - objs.apis.public = publicApi; - snapWithNormalizedDbname(publicApi); - - const [adminApi] = await pg.any( - `INSERT INTO services_public.apis (database_id, name, role_name, anon_role) - VALUES ($1, $2, $3, $4) - RETURNING *`, - [database_id, 'admin', 'administrator', 'administrator'] - ); - objs.apis.admin = adminApi; - snapWithNormalizedDbname(adminApi); - - // Step 3: Create sites - const [appSite] = await pg.any( - `INSERT INTO services_public.sites (database_id, title, description) - VALUES ($1, $2, $3) - RETURNING *`, - [database_id, 'Website Title', 'Website Description'] - ); - objs.sites.app = appSite; - snapWithNormalizedDbname(appSite); - - // Step 4: Register domains (linking to APIs and sites) - const [apiDomain] = await pg.any( - `INSERT INTO services_public.domains (database_id, api_id, domain, subdomain) - VALUES ($1, $2, $3, $4) - RETURNING *`, - [database_id, objs.apis.public.id, 'pgpm.io', 'api'] - ); - objs.domains.api = apiDomain; - expect(snapshot(apiDomain)).toMatchSnapshot(); - - const [appDomain] = await pg.any( - `INSERT INTO services_public.domains (database_id, site_id, domain, subdomain) - VALUES ($1, $2, $3, $4) - RETURNING *`, - [database_id, objs.sites.app.id, 'pgpm.io', 'app'] - ); - objs.domains.app = appDomain; - expect(snapshot(appDomain)).toMatchSnapshot(); - - const [adminDomain] = await pg.any( - `INSERT INTO services_public.domains (database_id, api_id, domain, subdomain) - VALUES ($1, $2, $3, $4) - RETURNING *`, - [database_id, objs.apis.admin.id, 'pgpm.io', 'admin'] - ); - objs.domains.admin = adminDomain; - expect(snapshot(adminDomain)).toMatchSnapshot(); - - const [baseDomain] = await pg.any( - `INSERT INTO services_public.domains (database_id, domain) - VALUES ($1, $2) - RETURNING *`, - [database_id, 'pgpm.io'] - ); - objs.domains.base = baseDomain; - - // Step 5: Register modules - const [siteModule1] = await pg.any( - `INSERT INTO services_public.site_modules (database_id, site_id, name, data) - VALUES ($1, $2, $3, $4::jsonb) - RETURNING *`, - [database_id, objs.sites.app.id, 'legal-emails', JSON.stringify({ - supportEmail: 'support@interweb.co' - })] - ); - expect(snapshot(siteModule1)).toMatchSnapshot(); - - const [apiModule] = await pg.any( - `INSERT INTO services_public.api_modules (database_id, api_id, name, data) - VALUES ($1, $2, $3, $4::jsonb) - RETURNING *`, - [database_id, objs.apis.public.id, 'rls_module', JSON.stringify({ - authenticate_schema: 'services_private', - authenticate: 'authenticate' - })] - ); - expect(snapshot(apiModule)).toMatchSnapshot(); - - const [siteModule2] = await pg.any( - `INSERT INTO services_public.site_modules (database_id, site_id, name, data) - VALUES ($1, $2, $3, $4::jsonb) - RETURNING *`, - [database_id, objs.sites.app.id, 'user_auth_module', JSON.stringify({ - auth_schema: 'services_public', - sign_in: 'login', - sign_up: 'register', - set_password: 'set_password', - reset_password: 'reset_password', - forgot_password: 'forgot_password', - send_verification_email: 'send_verification_email', - verify_email: 'verify_email' - })] - ); - expect(snapshot(siteModule2)).toMatchSnapshot(); - - // Step 6: Schema associations - const [schema] = await pg.any( - `INSERT INTO metaschema_public.schema (database_id, schema_name, name) - VALUES ($1, $2, $3) - RETURNING *`, - [database_id, 'brand-public', 'public'] - ); - - const [publicAssoc] = await pg.any( - `INSERT INTO services_public.api_schemas (database_id, schema_id, api_id) - VALUES ($1, $2, $3) - RETURNING *`, - [database_id, schema.id, objs.apis.public.id] - ); - - const [adminAssoc] = await pg.any( - `INSERT INTO services_public.api_schemas (database_id, schema_id, api_id) - VALUES ($1, $2, $3) - RETURNING *`, - [database_id, schema.id, objs.apis.admin.id] - ); - - snap(publicAssoc); - snap(adminAssoc); - }); - - // Individual component tests it('should create database independently', async () => { const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; @@ -202,28 +35,4 @@ describe('db_meta functionality', () => { expect(database.name).toBe('test-db'); expect(database.id).toBeDefined(); }); - - it('should register domain independently', async () => { - const owner_id = '07281002-1699-4762-57e3-ab1b92243120'; - - // Create database first - const [database] = await pg.any( - `INSERT INTO metaschema_public.database (owner_id, name) - VALUES ($1, $2) - RETURNING *`, - [owner_id, 'test-db-for-domain'] - ); - - // Then create domain - const [domain] = await pg.any( - `INSERT INTO services_public.domains (database_id, domain, subdomain) - VALUES ($1, $2, $3) - RETURNING *`, - [database.id, 'example.com', 'api'] - ); - - expect(domain.database_id).toBe(database.id); - expect(domain.domain).toBe('example.com'); - expect(domain.subdomain).toBe('api'); - }); }); diff --git a/packages/services/__tests__/__snapshots__/services.test.ts.snap b/packages/services/__tests__/__snapshots__/services.test.ts.snap deleted file mode 100644 index cf59d9275..000000000 --- a/packages/services/__tests__/__snapshots__/services.test.ts.snap +++ /dev/null @@ -1,147 +0,0 @@ -// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing - -exports[`services functionality should handle complete meta workflow with services 1`] = ` -{ - "hash": null, - "id": "[ID]", - "label": null, - "name": "my-meta-db", - "owner_id": "[ID]", - "private_schema_name": null, - "schema_hash": null, - "schema_name": null, -} -`; - -exports[`services functionality should handle complete meta workflow with services 2`] = ` -{ - "anon_role": "anonymous", - "database_id": "[ID]", - "dbname": "test-database", - "id": "[ID]", - "is_public": true, - "name": "public", - "role_name": "authenticated", -} -`; - -exports[`services functionality should handle complete meta workflow with services 3`] = ` -{ - "anon_role": "administrator", - "database_id": "[ID]", - "dbname": "test-database", - "id": "[ID]", - "is_public": true, - "name": "admin", - "role_name": "administrator", -} -`; - -exports[`services functionality should handle complete meta workflow with services 4`] = ` -{ - "apple_touch_icon": null, - "database_id": "[ID]", - "dbname": "test-database", - "description": "Website Description", - "favicon": null, - "id": "[ID]", - "logo": null, - "og_image": null, - "title": "Website Title", -} -`; - -exports[`services functionality should handle complete meta workflow with services 5`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "domain": "pgpm.io", - "id": "[ID]", - "site_id": null, - "subdomain": "api", -} -`; - -exports[`services functionality should handle complete meta workflow with services 6`] = ` -{ - "api_id": null, - "database_id": "[ID]", - "domain": "pgpm.io", - "id": "[ID]", - "site_id": "[ID]", - "subdomain": "app", -} -`; - -exports[`services functionality should handle complete meta workflow with services 7`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "domain": "pgpm.io", - "id": "[ID]", - "site_id": null, - "subdomain": "admin", -} -`; - -exports[`services functionality should handle complete meta workflow with services 8`] = ` -{ - "data": { - "supportEmail": "support@interweb.co", - }, - "database_id": "[ID]", - "id": "[ID]", - "name": "legal-emails", - "site_id": "[ID]", -} -`; - -exports[`services functionality should handle complete meta workflow with services 9`] = ` -{ - "api_id": "[ID]", - "data": { - "authenticate": "authenticate", - "authenticate_schema": "services_private", - }, - "database_id": "[ID]", - "id": "[ID]", - "name": "rls_module", -} -`; - -exports[`services functionality should handle complete meta workflow with services 10`] = ` -{ - "data": { - "auth_schema": "services_public", - "forgot_password": "forgot_password", - "reset_password": "reset_password", - "send_verification_email": "send_verification_email", - "set_password": "set_password", - "sign_in": "login", - "sign_up": "register", - "verify_email": "verify_email", - }, - "database_id": "[ID]", - "id": "[ID]", - "name": "user_auth_module", - "site_id": "[ID]", -} -`; - -exports[`services functionality should handle complete meta workflow with services 11`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "id": "[ID]", - "schema_id": "[ID]", -} -`; - -exports[`services functionality should handle complete meta workflow with services 12`] = ` -{ - "api_id": "[ID]", - "database_id": "[ID]", - "id": "[ID]", - "schema_id": "[ID]", -} -`; diff --git a/packages/types/Makefile b/packages/types/Makefile index d59535666..7141b96dd 100644 --- a/packages/types/Makefile +++ b/packages/types/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-types -DATA = sql/pgpm-types--0.15.5.sql +DATA = sql/pgpm-types--0.16.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/types/__tests__/domains.pgutils.test.ts b/packages/types/__tests__/domains.pgutils.test.ts index 7e01d98f1..f2e5c8156 100644 --- a/packages/types/__tests__/domains.pgutils.test.ts +++ b/packages/types/__tests__/domains.pgutils.test.ts @@ -1,78 +1,76 @@ import { getConnections, PgTestClient } from 'pgsql-test'; +// Validation rules: +// - url: lenient regex ^https?://[^\s]+$ (must start with http/https, no whitespace, paths allowed) +// - attachment: lenient regex ^https?://[^\s]+$ (same as url) +// - hostname: no whitespace (^[^\s]+$) +// - email: must contain @ (value ~ '@') +// - image: jsonb object requiring 'url' OR 'id' OR 'key', with type validation, optional bucket/provider/mime/versions (versions is array) +// - upload: jsonb object requiring 'url' OR 'id' OR 'key', with type validation on all fields, optional bucket/provider/mime + const validUrls = [ 'http://foo.com/blah_blah', 'http://foo.com/blah_blah/', 'http://foo.com/blah_blah_(wikipedia)', - 'http://foo.com/blah_blah_(wikipedia)_(again)', 'http://www.example.com/wpstyle/?p=364', 'https://www.example.com/foo/?bar=baz&inga=42&quux', - 'http://✪df.ws/123', 'http://foo.com/blah_(wikipedia)#cite-1', - 'http://foo.com/blah_(wikipedia)_blah#cite-1', 'http://foo.com/(something)?after=parens', 'http://code.google.com/events/#&product=browser', 'http://j.mp', 'http://foo.bar/?q=Test%20URL-encoded%20stuff', - 'http://مثال.إختبار', - 'http://例子.测试', - 'http://उदाहरण.परीक्षा', - "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", 'http://1337.net', 'http://a.b-c.de', 'https://foo_bar.example.com/' ]; const invalidUrls = [ - 'http://##', - 'http://##/', - 'http://foo.bar?q=Spaces should be encoded', - '//', - '//a', - '///a', - '///', - 'http:///a', 'foo.com', - 'rdar://1234', - 'h://test', - 'http:// shouldfail.com', - ':// should fail', - 'http://foo.bar/foo(bar)baz quux', - 'ftps://foo.bar/', - 'http://.www.foo.bar/', - 'http://.www.foo.bar./' -]; - -const validAttachments = [ - 'http://www.foo.bar/some.jpg', - 'https://foo.bar/some.PNG' -]; - -const invalidAttachments = [ - 'hi there', - 'ftp://foo.bar/some.png', - 'https:///foo.bar/some.png' + 'ftp://foo.bar/', + 'not-a-url', + 'random text with spaces', + '//missing-protocol.com' ]; const validImages = [ - { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, - { url: 'https://foo.bar/some.PNG', mime: 'image/jpg' } + { url: 'http://www.foo.bar/some.jpg' }, + { url: 'https://foo.bar/some.PNG' }, + { url: 'https://example.com/path/to/image.png' }, + { url: 'https://example.com/image.png', bucket: 'my-bucket' }, + { url: 'https://example.com/image.png', provider: 's3', mime: 'image/png' }, + { id: 'some-image-id' }, + { key: 'some-image-key' }, + { id: 'private-image', bucket: 'my-bucket', provider: 's3' }, + { url: 'https://example.com/image.png', versions: ['thumb', 'large'] } ]; const invalidImages = [ - { url: 'hi there' }, - { url: 'https://foo.bar/some.png' } + { notUrl: 'missing required keys' }, + { mime: 'only mime, no url/id/key' }, + { url: 'not-a-valid-url' }, + { url: 'ftp://wrong-protocol.com/image.png' }, + { id: 123 }, + { key: true }, + { url: 'https://example.com/image.png', bucket: 123 }, + { url: 'https://example.com/image.png', versions: 'not-an-array' } ]; const validUploads = [ - { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, - { url: 'https://foo.bar/some.PNG', mime: 'image/png' } + { url: 'http://www.foo.bar/some.jpg' }, + { url: 'https://foo.bar/some.PNG' }, + { id: 'some-id' }, + { key: 'some-key' }, + { url: 'https://example.com/file.pdf', id: 'with-id' }, + { id: 'some-id', bucket: 'my-bucket', provider: 's3', mime: 'application/pdf' } ]; const invalidUploads = [ - { url: 'hi there' }, - { url: 'https://foo.bar/some.png' }, - { url: 'ftp://foo.bar/some.png', mime: 'image/png' } + { notUrl: 'missing required keys' }, + { mime: 'only mime, no url/id/key' }, + { url: 'not-a-valid-url' }, + { url: 'ftp://wrong-protocol.com/file.pdf' }, + { id: 123 }, + { key: true } ]; let pg: PgTestClient; @@ -109,129 +107,152 @@ afterAll(async () => { }); describe('types', () => { - it('valid attachment and image', async () => { - for (const attachment of validAttachments) { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); - } - - for (const image of validImages) { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } - }); - - it('invalid attachment and image', async () => { - for (const attachment of invalidAttachments) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); - } catch (e) { - failed = true; + describe('url domain (lenient regex: ^https?://[^\\s]+$)', () => { + it('accepts valid URLs', async () => { + for (const value of validUrls) { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); } - expect(failed).toBe(true); - } + }); - for (const image of invalidImages) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } catch (e) { - failed = true; + it('rejects invalid URLs', async () => { + for (const value of invalidUrls) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); } - expect(failed).toBe(true); - } + }); }); - it('valid upload', async () => { - for (const upload of validUploads) { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } - }); + describe('hostname domain (no whitespace: ^[^\\s]+$)', () => { + it('accepts values without whitespace', async () => { + const values = [ + 'google.com', + 'www.example.com', + 'not-a-hostname', + 'http://with-protocol.com' + ]; + for (const value of values) { + await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); + } + }); - it('invalid upload', async () => { - for (const upload of invalidUploads) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } catch (e) { - failed = true; + it('rejects values with whitespace', async () => { + const invalidValues = [ + 'has spaces', + 'has\ttab' + ]; + for (const value of invalidValues) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); } - expect(failed).toBe(true); - } + }); }); - it('valid url', async () => { - for (const value of validUrls) { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } - }); + describe('attachment domain (lenient regex: ^https?://[^\\s]+$)', () => { + it('accepts valid URLs', async () => { + const values = [ + 'http://www.foo.bar/some.jpg', + 'https://foo.bar/some.PNG', + 'https://example.com/path/to/file.pdf' + ]; + for (const value of values) { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); + } + }); - it('invalid url', async () => { - for (const value of invalidUrls) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } catch (e) { - failed = true; + it('rejects invalid URLs', async () => { + const invalidValues = [ + 'not-a-url', + 'ftp://wrong-protocol.com/file.pdf', + 'random text with spaces' + ]; + for (const value of invalidValues) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); } - expect(failed).toBe(true); - } + }); }); - it('email', async () => { - await pg.any(` - INSERT INTO customers (email) VALUES - ('d@google.com'), - ('d@google.in'), - ('d@google.in'), - ('d@www.google.in'), - ('d@google.io'), - ('dan@google.some.other.com')`); - }); + describe('email domain (must contain @)', () => { + it('accepts values containing @', async () => { + const values = [ + 'd@google.com', + 'user@example.org', + 'test@localhost' + ]; + for (const value of values) { + await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); + } + }); - it('not email', async () => { - let failed = false; - try { - await pg.any(` - INSERT INTO customers (email) VALUES - ('http://google.some.other.com')`); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + it('rejects values without @', async () => { + const invalidValues = [ + 'not-an-email', + 'missing.at.sign' + ]; + for (const value of invalidValues) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); }); - it('hostname', async () => { - await pg.any(` - INSERT INTO customers (domain) VALUES - ('google.com'), - ('google.in'), - ('google.in'), - ('www.google.in'), - ('google.io'), - ('google.some.other.com')`); - }); + describe('image domain (jsonb requiring url OR id OR key, optional versions array)', () => { + it('accepts valid images with url, id, or key', async () => { + for (const image of validImages) { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } + }); - it('not hostname', async () => { - let failed = false; - try { - await pg.any(` - INSERT INTO customers (domain) VALUES - ('http://google.some.other.com')`); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + it('rejects invalid images', async () => { + for (const image of invalidImages) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); }); - it('not hostname 2', async () => { - let failed = false; - try { - await pg.any(` - INSERT INTO customers (domain) VALUES - ('google.some.other.com/a/b/d')`); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + describe('upload domain (jsonb requiring url OR id OR key)', () => { + it('accepts valid uploads with url, id, or key', async () => { + for (const upload of validUploads) { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } + }); + + it('rejects uploads without url, id, or key', async () => { + for (const upload of invalidUploads) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); }); }); diff --git a/packages/types/__tests__/domains.test.ts b/packages/types/__tests__/domains.test.ts index 7e01d98f1..6594f302e 100644 --- a/packages/types/__tests__/domains.test.ts +++ b/packages/types/__tests__/domains.test.ts @@ -1,78 +1,113 @@ import { getConnections, PgTestClient } from 'pgsql-test'; +// Validation rules: +// - url: lenient regex ^https?://[^\s]+$ (must start with http/https, no whitespace, paths allowed) +// - origin: strict regex ^https?://[^/\s]+$ (protocol + host only, no paths for CORS security) +// - attachment: lenient regex ^https?://[^\s]+$ (same as url) +// - hostname: no whitespace (^[^\s]+$) +// - email: must contain @ (value ~ '@') +// - image: jsonb object requiring 'url' OR 'id' OR 'key', with type validation, optional bucket/provider/mime/versions (versions is array) +// - upload: jsonb object requiring 'url' OR 'id' OR 'key', with type validation on all fields, optional bucket/provider/mime + const validUrls = [ 'http://foo.com/blah_blah', 'http://foo.com/blah_blah/', 'http://foo.com/blah_blah_(wikipedia)', - 'http://foo.com/blah_blah_(wikipedia)_(again)', 'http://www.example.com/wpstyle/?p=364', 'https://www.example.com/foo/?bar=baz&inga=42&quux', - 'http://✪df.ws/123', 'http://foo.com/blah_(wikipedia)#cite-1', - 'http://foo.com/blah_(wikipedia)_blah#cite-1', 'http://foo.com/(something)?after=parens', 'http://code.google.com/events/#&product=browser', 'http://j.mp', 'http://foo.bar/?q=Test%20URL-encoded%20stuff', - 'http://مثال.إختبار', - 'http://例子.测试', - 'http://उदाहरण.परीक्षा', - "http://-.~_!$&'()*+,;=:%40:80%2f::::::@example.com", 'http://1337.net', 'http://a.b-c.de', 'https://foo_bar.example.com/' ]; const invalidUrls = [ - 'http://##', - 'http://##/', - 'http://foo.bar?q=Spaces should be encoded', - '//', - '//a', - '///a', - '///', - 'http:///a', 'foo.com', - 'rdar://1234', - 'h://test', - 'http:// shouldfail.com', - ':// should fail', - 'http://foo.bar/foo(bar)baz quux', - 'ftps://foo.bar/', - 'http://.www.foo.bar/', - 'http://.www.foo.bar./' + 'ftp://foo.bar/', + 'not-a-url', + 'random text with spaces', + '//missing-protocol.com' ]; -const validAttachments = [ - 'http://www.foo.bar/some.jpg', - 'https://foo.bar/some.PNG' +// Valid origins: protocol + host only (no paths) +const validOrigins = [ + 'http://example.com', + 'https://example.com', + 'http://localhost:3000', + 'https://api.example.com:8080', + 'http://192.168.1.1', + 'https://foo_bar.example.com' ]; -const invalidAttachments = [ - 'hi there', - 'ftp://foo.bar/some.png', - 'https:///foo.bar/some.png' +// Invalid origins: paths, query strings, fragments, or non-http protocols +const invalidOrigins = [ + 'https://example.com/', + 'https://example.com/path', + 'https://example.com/malicious/path', + 'https://example.com?query=1', + 'https://example.com#fragment', + 'ftp://example.com', + 'foo.com', + 'not-an-origin' ]; const validImages = [ - { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, - { url: 'https://foo.bar/some.PNG', mime: 'image/jpg' } + { url: 'http://www.foo.bar/some.jpg' }, + { url: 'https://foo.bar/some.PNG' }, + { url: 'https://example.com/path/to/image.png' }, + { url: 'https://example.com/image.png', bucket: 'my-bucket' }, + { url: 'https://example.com/image.png', provider: 's3' }, + { url: 'https://example.com/image.png', mime: 'image/png' }, + { url: 'https://example.com/image.png', bucket: 'my-bucket', provider: 's3', mime: 'image/jpeg' }, + { id: 'some-image-id' }, + { key: 'some-image-key' }, + { id: 'private-image', bucket: 'my-bucket', provider: 's3' }, + { url: 'https://example.com/image.png', versions: ['thumb', 'medium', 'large'] }, + { id: 'image-with-versions', versions: [{ size: 'thumb' }, { size: 'large' }] } ]; const invalidImages = [ - { url: 'hi there' }, - { url: 'https://foo.bar/some.png' } + { notUrl: 'missing required keys' }, + { mime: 'only mime, no url/id/key' }, + { url: 'not-a-valid-url' }, + { url: 'ftp://wrong-protocol.com/image.png' }, + { id: 123 }, + { key: true }, + { url: 'https://example.com/image.png', bucket: 123 }, + { url: 'https://example.com/image.png', provider: true }, + { url: 'https://example.com/image.png', mime: ['array'] }, + { url: 'https://example.com/image.png', versions: 'not-an-array' }, + 'not-an-object', + ['array-not-object'] ]; const validUploads = [ - { url: 'http://www.foo.bar/some.jpg', mime: 'image/jpg' }, - { url: 'https://foo.bar/some.PNG', mime: 'image/png' } + { url: 'http://www.foo.bar/some.jpg' }, + { url: 'https://foo.bar/some.PNG' }, + { id: 'some-id' }, + { key: 'some-key' }, + { url: 'https://example.com/file.pdf', id: 'with-id' }, + { id: 'some-id', bucket: 'my-bucket', provider: 's3' }, + { key: 'some-key', mime: 'application/pdf' }, + { url: 'https://example.com/file.pdf', bucket: 'bucket', provider: 'gcs', mime: 'application/pdf' } ]; const invalidUploads = [ - { url: 'hi there' }, - { url: 'https://foo.bar/some.png' }, - { url: 'ftp://foo.bar/some.png', mime: 'image/png' } + { notUrl: 'missing required keys' }, + { mime: 'only mime, no url/id/key' }, + { url: 'not-a-valid-url' }, + { url: 'ftp://wrong-protocol.com/file.pdf' }, + { id: 123 }, + { key: true }, + { url: 'https://example.com/file.pdf', bucket: 123 }, + { url: 'https://example.com/file.pdf', provider: ['array'] }, + { id: 'some-id', mime: { nested: 'object' } }, + 'not-an-object', + ['array-not-object'] ]; let pg: PgTestClient; @@ -87,6 +122,7 @@ beforeAll(async () => { CREATE TABLE customers ( id serial, url url, + origin origin, image image, attachment attachment, domain hostname, @@ -109,129 +145,176 @@ afterAll(async () => { }); describe('types', () => { - it('valid attachment and image', async () => { - for (const attachment of validAttachments) { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); - } - - for (const image of validImages) { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } + describe('url domain (lenient regex: ^https?://[^\\s]+$)', () => { + it('accepts valid URLs', async () => { + for (const value of validUrls) { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); + } + }); + + it('rejects invalid URLs', async () => { + for (const value of invalidUrls) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); }); - it('invalid attachment and image', async () => { - for (const attachment of invalidAttachments) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [attachment]); - } catch (e) { - failed = true; + describe('origin domain (strict regex: ^https?://[^/\\s]+$ - no paths)', () => { + it('accepts valid origins (protocol + host only)', async () => { + for (const value of validOrigins) { + await pg.any(`INSERT INTO customers (origin) VALUES ($1);`, [value]); } - expect(failed).toBe(true); - } + }); - for (const image of invalidImages) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); - } catch (e) { - failed = true; + it('rejects origins with paths, query strings, or invalid protocols', async () => { + for (const value of invalidOrigins) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (origin) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); } - expect(failed).toBe(true); - } + }); }); - it('valid upload', async () => { - for (const upload of validUploads) { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } - }); + describe('hostname domain (no whitespace: ^[^\\s]+$)', () => { + it('accepts values without whitespace', async () => { + const values = [ + 'google.com', + 'www.example.com', + 'not-a-hostname', + 'http://with-protocol.com', + 'anything-without-spaces' + ]; + for (const value of values) { + await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); + } + }); - it('invalid upload', async () => { - for (const upload of invalidUploads) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); - } catch (e) { - failed = true; + it('rejects values with whitespace', async () => { + const invalidValues = [ + 'has spaces', + 'has\ttab', + 'has\nnewline' + ]; + for (const value of invalidValues) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (domain) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); } - expect(failed).toBe(true); - } + }); }); - it('valid url', async () => { - for (const value of validUrls) { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } - }); + describe('attachment domain (lenient regex: ^https?://[^\\s]+$)', () => { + it('accepts valid URLs', async () => { + const values = [ + 'http://www.foo.bar/some.jpg', + 'https://foo.bar/some.PNG', + 'https://example.com/path/to/file.pdf' + ]; + for (const value of values) { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); + } + }); - it('invalid url', async () => { - for (const value of invalidUrls) { - let failed = false; - try { - await pg.any(`INSERT INTO customers (url) VALUES ($1);`, [value]); - } catch (e) { - failed = true; + it('rejects invalid URLs', async () => { + const invalidValues = [ + 'not-a-url', + 'ftp://wrong-protocol.com/file.pdf', + 'random text with spaces' + ]; + for (const value of invalidValues) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (attachment) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); } - expect(failed).toBe(true); - } + }); }); - it('email', async () => { - await pg.any(` - INSERT INTO customers (email) VALUES - ('d@google.com'), - ('d@google.in'), - ('d@google.in'), - ('d@www.google.in'), - ('d@google.io'), - ('dan@google.some.other.com')`); - }); + describe('email domain (must contain @)', () => { + it('accepts values containing @', async () => { + const values = [ + 'd@google.com', + 'user@example.org', + 'test@localhost', + 'weird@but@valid' + ]; + for (const value of values) { + await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); + } + }); - it('not email', async () => { - let failed = false; - try { - await pg.any(` - INSERT INTO customers (email) VALUES - ('http://google.some.other.com')`); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + it('rejects values without @', async () => { + const invalidValues = [ + 'not-an-email', + 'random text', + 'missing.at.sign' + ]; + for (const value of invalidValues) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (email) VALUES ($1);`, [value]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); }); - it('hostname', async () => { - await pg.any(` - INSERT INTO customers (domain) VALUES - ('google.com'), - ('google.in'), - ('google.in'), - ('www.google.in'), - ('google.io'), - ('google.some.other.com')`); - }); + describe('image domain (jsonb requiring url OR id OR key, optional versions array)', () => { + it('accepts valid images with url, id, or key', async () => { + for (const image of validImages) { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } + }); - it('not hostname', async () => { - let failed = false; - try { - await pg.any(` - INSERT INTO customers (domain) VALUES - ('http://google.some.other.com')`); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + it('rejects invalid images', async () => { + for (const image of invalidImages) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (image) VALUES ($1::json);`, [image]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); }); - it('not hostname 2', async () => { - let failed = false; - try { - await pg.any(` - INSERT INTO customers (domain) VALUES - ('google.some.other.com/a/b/d')`); - } catch (e) { - failed = true; - } - expect(failed).toBe(true); + describe('upload domain (jsonb requiring url OR id OR key)', () => { + it('accepts valid uploads with url, id, or key', async () => { + for (const upload of validUploads) { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } + }); + + it('rejects uploads without url, id, or key', async () => { + for (const upload of invalidUploads) { + let failed = false; + try { + await pg.any(`INSERT INTO customers (upload) VALUES ($1::json);`, [upload]); + } catch (e) { + failed = true; + } + expect(failed).toBe(true); + } + }); }); }); diff --git a/packages/types/pgpm-types.control b/packages/types/pgpm-types.control index 7c098cbe3..ce373f4f4 100644 --- a/packages/types/pgpm-types.control +++ b/packages/types/pgpm-types.control @@ -1,6 +1,6 @@ # pgpm-types extension comment = 'pgpm-types extension' -default_version = '0.15.5' +default_version = '0.16.0' module_pathname = '$libdir/pgpm-types' requires = 'plpgsql,citext,pgpm-verify' relocatable = false From 0b3e7a79da8a0d75516d56ffe027812b343aebbb Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Thu, 26 Feb 2026 11:36:42 +0000 Subject: [PATCH 3/3] fix: revert structural SQL changes in metaschema-modules, restore original tests and snapshots - Revert table_module structural changes (private_schema_id->schema_id rename, new columns) - Revert rls_module structural changes (tokens_table_id->session_credentials_table_id rename) - Revert profiles_module structural change (removed bitlen column) - Remove relation_provision new table (not yet supported by existing tests) - Restore original metaschema-modules test file, snapshots, pgpm.plan, and SQL version - Restore original services snapshot file - Keep all COMMENT ON additions and non-breaking changes (field_module comments, table_template_module comments) --- .../__snapshots__/modules.test.ts.snap | 473 ++++++++++++++++++ .../__tests__/modules.test.ts | 8 +- .../tables/profiles_module/table.sql | 2 + .../tables/relation_provision/table.sql | 282 ----------- .../tables/rls_module/table.sql | 9 +- .../tables/table_module/table.sql | 17 +- packages/metaschema-modules/pgpm.plan | 3 +- .../tables/field_module/table.sql | 2 +- .../tables/relation_provision/table.sql | 7 - .../sql/metaschema-modules--0.15.5.sql | 426 ++++------------ .../tables/field_module/table.sql | 4 +- .../tables/profiles_module/table.sql | 2 +- .../tables/relation_provision/table.sql | 36 -- .../tables/table_module/table.sql | 4 +- .../__snapshots__/services.test.ts.snap | 147 ++++++ 15 files changed, 739 insertions(+), 683 deletions(-) create mode 100644 packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap delete mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql delete mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql delete mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql create mode 100644 packages/services/__tests__/__snapshots__/services.test.ts.snap diff --git a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap new file mode 100644 index 000000000..459a5bc63 --- /dev/null +++ b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap @@ -0,0 +1,473 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`db_meta_modules should have all expected module tables 1`] = ` +{ + "moduleNames": [ + "connected_accounts_module", + "crypto_addresses_module", + "crypto_auth_module", + "default_ids_module", + "emails_module", + "encrypted_secrets_module", + "field_module", + "hierarchy_module", + "invites_module", + "levels_module", + "limits_module", + "membership_types_module", + "memberships_module", + "permissions_module", + "phone_numbers_module", + "profiles_module", + "rls_module", + "secrets_module", + "sessions_module", + "table_module", + "table_template_module", + "user_auth_module", + "users_module", + "uuid_module", + ], +} +`; + +exports[`db_meta_modules should verify all module tables exist in metaschema_modules_public schema 1`] = ` +{ + "moduleTablesCount": 24, + "totalTables": 26, +} +`; + +exports[`db_meta_modules should verify emails_module table structure 1`] = ` +{ + "columns": [ + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "database_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "private_schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "owner_table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "table_name", + "data_type": "text", + "is_nullable": "NO", + }, + ], +} +`; + +exports[`db_meta_modules should verify field_module table structure 1`] = ` +{ + "columns": [ + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "database_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "private_schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "field_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "node_type", + "data_type": "text", + "is_nullable": "NO", + }, + { + "column_default": "'{}'::jsonb", + "column_name": "data", + "data_type": "jsonb", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "triggers", + "data_type": "ARRAY", + "is_nullable": "YES", + }, + { + "column_default": null, + "column_name": "functions", + "data_type": "ARRAY", + "is_nullable": "YES", + }, + ], +} +`; + +exports[`db_meta_modules should verify module table structures have database_id foreign keys 1`] = ` +{ + "constraintCount": 72600, +} +`; + +exports[`db_meta_modules should verify module tables have proper foreign key relationships 1`] = ` +{ + "constraintCount": 102535, + "foreignTables": [ + "apis", + "database", + "field", + "schema", + "table", + ], +} +`; + +exports[`db_meta_modules should verify specific module table column defaults 1`] = ` +{ + "sessionsDefaults": [ + { + "column_default": "'app_auth_settings'::text", + "column_name": "auth_settings_table", + }, + { + "column_default": "uuid_nil()", + "column_name": "auth_settings_table_id", + }, + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + }, + { + "column_default": "uuid_nil()", + "column_name": "schema_id", + }, + { + "column_default": "'session_credentials'::text", + "column_name": "session_credentials_table", + }, + { + "column_default": "uuid_nil()", + "column_name": "session_credentials_table_id", + }, + { + "column_default": "'30 days'::interval", + "column_name": "sessions_default_expiration", + }, + { + "column_default": "'sessions'::text", + "column_name": "sessions_table", + }, + { + "column_default": "uuid_nil()", + "column_name": "sessions_table_id", + }, + { + "column_default": "uuid_nil()", + "column_name": "users_table_id", + }, + ], + "usersDefaults": [ + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + }, + { + "column_default": "uuid_nil()", + "column_name": "schema_id", + }, + { + "column_default": "uuid_nil()", + "column_name": "table_id", + }, + { + "column_default": "'users'::text", + "column_name": "table_name", + }, + { + "column_default": "uuid_nil()", + "column_name": "type_table_id", + }, + { + "column_default": "'role_types'::text", + "column_name": "type_table_name", + }, + ], +} +`; + +exports[`db_meta_modules should verify table_module table structure 1`] = ` +{ + "columns": [ + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "database_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "private_schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "node_type", + "data_type": "text", + "is_nullable": "NO", + }, + { + "column_default": "'{}'::jsonb", + "column_name": "data", + "data_type": "jsonb", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "fields", + "data_type": "ARRAY", + "is_nullable": "YES", + }, + ], +} +`; + +exports[`db_meta_modules should verify table_template_module table structure 1`] = ` +{ + "columns": [ + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "database_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "private_schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "owner_table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "table_name", + "data_type": "text", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "node_type", + "data_type": "text", + "is_nullable": "NO", + }, + { + "column_default": "'{}'::jsonb", + "column_name": "data", + "data_type": "jsonb", + "is_nullable": "NO", + }, + ], +} +`; + +exports[`db_meta_modules should verify sessions_module table structure 1`] = ` +{ + "columns": [ + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "database_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "sessions_table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "session_credentials_table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "auth_settings_table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "users_table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "'30 days'::interval", + "column_name": "sessions_default_expiration", + "data_type": "interval", + "is_nullable": "NO", + }, + { + "column_default": "'sessions'::text", + "column_name": "sessions_table", + "data_type": "text", + "is_nullable": "NO", + }, + { + "column_default": "'session_credentials'::text", + "column_name": "session_credentials_table", + "data_type": "text", + "is_nullable": "NO", + }, + { + "column_default": "'app_auth_settings'::text", + "column_name": "auth_settings_table", + "data_type": "text", + "is_nullable": "NO", + }, + ], +} +`; + +exports[`db_meta_modules should verify users_module table structure 1`] = ` +{ + "columns": [ + { + "column_default": "uuid_generate_v4()", + "column_name": "id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": null, + "column_name": "database_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "schema_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "'users'::text", + "column_name": "table_name", + "data_type": "text", + "is_nullable": "NO", + }, + { + "column_default": "uuid_nil()", + "column_name": "type_table_id", + "data_type": "uuid", + "is_nullable": "NO", + }, + { + "column_default": "'role_types'::text", + "column_name": "type_table_name", + "data_type": "text", + "is_nullable": "NO", + }, + ], +} +`; diff --git a/packages/metaschema-modules/__tests__/modules.test.ts b/packages/metaschema-modules/__tests__/modules.test.ts index 9fa747d16..e202d0aac 100644 --- a/packages/metaschema-modules/__tests__/modules.test.ts +++ b/packages/metaschema-modules/__tests__/modules.test.ts @@ -224,7 +224,7 @@ describe('db_meta_modules', () => { constraintCount: fkConstraints.length, foreignTables: foreignTables.sort() })).toMatchSnapshot(); - }); + }, 30000); it('should verify specific module table column defaults', async () => { // Check that modules have sensible defaults @@ -302,11 +302,9 @@ describe('db_meta_modules', () => { const columnNames = columns.map(c => c.column_name); expect(columnNames).toContain('id'); expect(columnNames).toContain('database_id'); - expect(columnNames).toContain('schema_id'); + expect(columnNames).toContain('private_schema_id'); expect(columnNames).toContain('table_id'); - expect(columnNames).toContain('table_name'); expect(columnNames).toContain('node_type'); - expect(columnNames).toContain('use_rls'); expect(columnNames).toContain('data'); expect(columnNames).toContain('fields'); @@ -341,4 +339,4 @@ describe('db_meta_modules', () => { expect(snapshot({ columns })).toMatchSnapshot(); }); -}); \ No newline at end of file +}); \ No newline at end of file diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql index 7c6f8b4c0..73754b2f4 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -27,6 +27,8 @@ CREATE TABLE metaschema_modules_public.profiles_module ( profile_definition_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), profile_definition_grants_table_name text NOT NULL DEFAULT '', + -- Configuration + bitlen int NOT NULL DEFAULT 24, membership_type int NOT NULL, -- Entity table for org/group scoped profiles (NULL for app-level) diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql deleted file mode 100644 index 0692c49cd..000000000 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/relation_provision/table.sql +++ /dev/null @@ -1,282 +0,0 @@ --- Deploy schemas/metaschema_modules_public/tables/relation_provision/table to pg - --- requires: schemas/metaschema_modules_public/schema - -BEGIN; - -CREATE TABLE metaschema_modules_public.relation_provision ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), - - database_id uuid NOT NULL, - - -- ========================================================================= - -- Relation type and tables - -- ========================================================================= - - relation_type text NOT NULL CHECK (relation_type IN ( - 'RelationBelongsTo', 'RelationHasOne', 'RelationHasMany', 'RelationManyToMany' - )), - - source_table_id uuid NOT NULL, - - target_table_id uuid NOT NULL, - - -- ========================================================================= - -- BelongsTo / HasOne / HasMany: FK field config - -- ========================================================================= - - field_name text DEFAULT NULL, - - delete_action text DEFAULT NULL, - - is_required boolean NOT NULL DEFAULT true, - - -- ========================================================================= - -- ManyToMany: junction table identity - -- ========================================================================= - - junction_table_id uuid NOT NULL DEFAULT uuid_nil(), - - junction_table_name text DEFAULT NULL, - - junction_schema_id uuid DEFAULT NULL, - - source_field_name text DEFAULT NULL, - - target_field_name text DEFAULT NULL, - - -- ========================================================================= - -- ManyToMany: junction table primary key strategy - -- ========================================================================= - - use_composite_key boolean NOT NULL DEFAULT false, - - -- ========================================================================= - -- ManyToMany: field creation (forwarded to secure_table_provision) - -- ========================================================================= - - node_type text DEFAULT NULL, - - node_data jsonb NOT NULL DEFAULT '{}', - - -- ========================================================================= - -- ManyToMany: grants (forwarded to secure_table_provision) - -- ========================================================================= - - grant_roles text[] NOT NULL DEFAULT ARRAY['authenticated'], - - grant_privileges jsonb NOT NULL DEFAULT '[["select","*"],["insert","*"],["delete","*"]]', - - -- ========================================================================= - -- ManyToMany: RLS policies (forwarded to secure_table_provision) - -- ========================================================================= - - policy_type text DEFAULT NULL, - - policy_privileges text[] DEFAULT NULL, - - policy_role text DEFAULT NULL, - - policy_permissive boolean NOT NULL DEFAULT true, - - policy_data jsonb NOT NULL DEFAULT '{}', - - -- ========================================================================= - -- Output columns (populated by the trigger, not set by callers) - -- ========================================================================= - - out_field_id uuid DEFAULT NULL, - - out_junction_table_id uuid DEFAULT NULL, - - out_source_field_id uuid DEFAULT NULL, - - out_target_field_id uuid DEFAULT NULL, - - CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, - CONSTRAINT source_table_fkey FOREIGN KEY (source_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT target_table_fkey FOREIGN KEY (target_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE -); - --- ============================================================================= --- Table-level comment --- ============================================================================= - -COMMENT ON TABLE metaschema_modules_public.relation_provision IS - 'Provisions relational structure between tables. Supports four relation types: - - RelationBelongsTo: adds a FK field on the source table referencing the target table (child perspective: "tasks belongs to projects" -> tasks.project_id). - - RelationHasMany: adds a FK field on the target table referencing the source table (parent perspective: "projects has many tasks" -> tasks.project_id). Inverse of BelongsTo. - - RelationHasOne: adds a FK field with a unique constraint on the source table referencing the target table. Also supports shared-primary-key patterns where the FK field IS the primary key (set field_name to the existing PK field name). - - RelationManyToMany: creates a junction table with FK fields to both source and target tables, delegating table creation and security to secure_table_provision. - This is a one-and-done structural provisioner. To layer additional security onto junction tables after creation, use secure_table_provision directly. - All operations are graceful: existing fields, FK constraints, and unique constraints are reused if found. - The trigger never injects values the caller did not provide. All security config is forwarded to secure_table_provision as-is.'; - --- ============================================================================= --- Relation type and tables --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.id IS - 'Unique identifier for this relation provision row.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.database_id IS - 'The database this relation belongs to. Required. Must match the database of both source_table_id and target_table_id.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.relation_type IS - 'The type of relation to create. Uses SuperCase naming matching the node_type_registry: - - RelationBelongsTo: creates a FK field on source_table referencing target_table (e.g., tasks belongs to projects -> tasks.project_id). Field name auto-derived from target table. - - RelationHasMany: creates a FK field on target_table referencing source_table (e.g., projects has many tasks -> tasks.project_id). Field name auto-derived from source table. Inverse of BelongsTo — same FK, different perspective. - - RelationHasOne: creates a FK field + unique constraint on source_table referencing target_table (e.g., user_settings has one user -> user_settings.user_id with UNIQUE). Also supports shared-primary-key patterns (e.g., user_profiles.id = users.id) by setting field_name to the existing PK field. - - RelationManyToMany: creates a junction table with FK fields to both tables (e.g., projects and tags -> project_tags table). - Each relation type uses a different subset of columns on this table. Required.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_table_id IS - 'The source table in the relation. Required. - - RelationBelongsTo: the table that receives the FK field (e.g., tasks in "tasks belongs to projects"). - - RelationHasMany: the parent table being referenced (e.g., projects in "projects has many tasks"). The FK field is created on the target table. - - RelationHasOne: the table that receives the FK field + unique constraint (e.g., user_settings in "user_settings has one user"). - - RelationManyToMany: one of the two tables being joined (e.g., projects in "projects and tags"). The junction table will have a FK field referencing this table.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_table_id IS - 'The target table in the relation. Required. - - RelationBelongsTo: the table being referenced by the FK (e.g., projects in "tasks belongs to projects"). - - RelationHasMany: the table that receives the FK field (e.g., tasks in "projects has many tasks"). - - RelationHasOne: the table being referenced by the FK (e.g., users in "user_settings has one user"). - - RelationManyToMany: the other table being joined (e.g., tags in "projects and tags"). The junction table will have a FK field referencing this table.'; - --- ============================================================================= --- BelongsTo / HasOne / HasMany: FK field config --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.field_name IS - 'FK field name for RelationBelongsTo, RelationHasOne, and RelationHasMany. - - RelationBelongsTo/RelationHasOne: if NULL, auto-derived from the target table name (e.g., target "projects" derives "project_id"). - - RelationHasMany: if NULL, auto-derived from the source table name (e.g., source "projects" derives "project_id"). - For RelationHasOne shared-primary-key patterns, set field_name to the existing PK field (e.g., "id") so the FK reuses it. - Ignored for RelationManyToMany — use source_field_name/target_field_name instead.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.delete_action IS - 'FK delete action for RelationBelongsTo, RelationHasOne, and RelationHasMany. One of: c (CASCADE), r (RESTRICT), n (SET NULL), d (SET DEFAULT), a (NO ACTION). Required — the trigger raises an error if not provided. The caller must explicitly choose the cascade behavior; there is no default. Ignored for RelationManyToMany (junction FK fields always use CASCADE).'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.is_required IS - 'Whether the FK field is NOT NULL. Defaults to true. - - RelationBelongsTo: set to false for optional associations (e.g., tasks.assignee_id that can be NULL). - - RelationHasMany: set to false if the child can exist without a parent. - - RelationHasOne: typically true. - Ignored for RelationManyToMany (junction FK fields are always required).'; - --- ============================================================================= --- ManyToMany: junction table identity --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_id IS - 'For RelationManyToMany: an existing junction table to use. Defaults to uuid_nil(). - - When uuid_nil(): the trigger creates a new junction table via secure_table_provision using junction_table_name. - - When set to a valid table UUID: the trigger skips table creation and only adds FK fields, composite key (if use_composite_key is true), and security to the existing table. - Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_table_name IS - 'For RelationManyToMany: name of the junction table to create or look up. If NULL, auto-derived from source and target table names using inflection_db (e.g., "projects" + "tags" derives "project_tags"). Only used when junction_table_id is uuid_nil(). Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.junction_schema_id IS - 'For RelationManyToMany: schema for the junction table. If NULL, defaults to the source table''s schema. Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.source_field_name IS - 'For RelationManyToMany: FK field name on the junction table referencing the source table. If NULL, auto-derived from the source table name using inflection_db.get_foreign_key_field_name() (e.g., source table "projects" derives "project_id"). Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.target_field_name IS - 'For RelationManyToMany: FK field name on the junction table referencing the target table. If NULL, auto-derived from the target table name using inflection_db.get_foreign_key_field_name() (e.g., target table "tags" derives "tag_id"). Ignored for RelationBelongsTo/RelationHasOne.'; - --- ============================================================================= --- ManyToMany: junction table primary key strategy --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.use_composite_key IS - 'For RelationManyToMany: whether to create a composite primary key from the two FK fields (source + target) on the junction table. Defaults to false. - - When true: the trigger calls metaschema.pk() with ARRAY[source_field_id, target_field_id] to create a composite PK. No separate id column is created. This enforces uniqueness of the pair and is suitable for simple junction tables. - - When false: no primary key is created by the trigger. The caller should provide node_type=''DataId'' to create a UUID primary key, or handle the PK strategy via a separate secure_table_provision row. - use_composite_key and node_type=''DataId'' are mutually exclusive — using both would create two conflicting PKs. - Ignored for RelationBelongsTo/RelationHasOne.'; - --- ============================================================================= --- ManyToMany: field creation (forwarded to secure_table_provision) --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.node_type IS - 'For RelationManyToMany: which generator to invoke for field creation on the junction table. Forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. - Examples: DataId (creates UUID primary key), DataDirectOwner (creates owner_id field), DataEntityMembership (creates entity_id field), DataOwnershipInEntity (creates both owner_id and entity_id), DataTimestamps, DataPeoplestamps, DataPublishable, DataSoftDelete. - NULL means no field creation beyond the FK fields (and composite key if use_composite_key is true). - Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.node_data IS - 'For RelationManyToMany: configuration passed to the generator function for field creation on the junction table. Forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. - Only used when node_type is set. Structure varies by node_type. Examples: - - DataId: {"field_name": "id"} (default field name is ''id'') - - DataEntityMembership: {"entity_field_name": "entity_id", "include_id": false, "include_user_fk": true} - - DataDirectOwner: {"owner_field_name": "owner_id"} - Defaults to ''{}'' (empty object). - Ignored for RelationBelongsTo/RelationHasOne.'; - --- ============================================================================= --- ManyToMany: grants (forwarded to secure_table_provision) --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.grant_roles IS - 'For RelationManyToMany: database roles to grant privileges to on the junction table. Forwarded to secure_table_provision as-is. Supports multiple roles, e.g. ARRAY[''authenticated'', ''admin'']. Each role receives all privileges defined in grant_privileges. Defaults to ARRAY[''authenticated'']. Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.grant_privileges IS - 'For RelationManyToMany: privilege grants for the junction table. Forwarded to secure_table_provision as-is. Format: array of [privilege, columns] tuples. Examples: [["select","*"],["insert","*"]] for full access, or [["update",["name","bio"]]] for column-level grants. "*" means all columns. Defaults to select/insert/delete for all columns. Ignored for RelationBelongsTo/RelationHasOne.'; - --- ============================================================================= --- ManyToMany: RLS policies (forwarded to secure_table_provision) --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_type IS - 'For RelationManyToMany: RLS policy type for the junction table. Forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. - Examples: AuthzEntityMembership, AuthzMembership, AuthzAllowAll, AuthzDirectOwner, AuthzOrgHierarchy. - NULL means no policy is created — the junction table will have RLS enabled but no policies (unless added separately via secure_table_provision). - Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_privileges IS - 'For RelationManyToMany: privileges the policy applies to, e.g. ARRAY[''select'',''insert'',''delete'']. Forwarded to secure_table_provision as-is. NULL means privileges are derived from the grant_privileges verbs by secure_table_provision. Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_role IS - 'For RelationManyToMany: database role the policy targets, e.g. ''authenticated''. Forwarded to secure_table_provision as-is. NULL means secure_table_provision falls back to the first role in grant_roles. Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_permissive IS - 'For RelationManyToMany: whether the policy is PERMISSIVE (true) or RESTRICTIVE (false). Forwarded to secure_table_provision as-is. Defaults to true. Ignored for RelationBelongsTo/RelationHasOne.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.policy_data IS - 'For RelationManyToMany: opaque policy configuration forwarded to secure_table_provision as-is. The trigger does not interpret or validate this value. Structure varies by policy_type. Examples: - - AuthzEntityMembership: {"entity_field": "entity_id", "membership_type": 2} - - AuthzDirectOwner: {"owner_field": "owner_id"} - - AuthzMembership: {"membership_type": 2} - Defaults to ''{}'' (empty object). - Ignored for RelationBelongsTo/RelationHasOne.'; - --- ============================================================================= --- Output columns --- ============================================================================= - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_field_id IS - 'Output column for RelationBelongsTo/RelationHasOne/RelationHasMany: the UUID of the FK field created (or found). For BelongsTo/HasOne this is on the source table; for HasMany this is on the target table. Populated by the trigger. NULL for RelationManyToMany. Callers should not set this directly.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_junction_table_id IS - 'Output column for RelationManyToMany: the UUID of the junction table created (or found). Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_source_field_id IS - 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the source table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; - -COMMENT ON COLUMN metaschema_modules_public.relation_provision.out_target_field_id IS - 'Output column for RelationManyToMany: the UUID of the FK field on the junction table referencing the target table. Populated by the trigger. NULL for RelationBelongsTo/RelationHasOne. Callers should not set this directly.'; - -COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.relation_provision IS E'@omit manyToMany'; -COMMENT ON CONSTRAINT source_table_fkey ON metaschema_modules_public.relation_provision IS E'@omit manyToMany'; -COMMENT ON CONSTRAINT target_table_fkey ON metaschema_modules_public.relation_provision IS E'@omit manyToMany'; - -CREATE INDEX relation_provision_database_id_idx ON metaschema_modules_public.relation_provision ( database_id ); -CREATE INDEX relation_provision_relation_type_idx ON metaschema_modules_public.relation_provision ( relation_type ); -CREATE INDEX relation_provision_source_table_id_idx ON metaschema_modules_public.relation_provision ( source_table_id ); -CREATE INDEX relation_provision_target_table_id_idx ON metaschema_modules_public.relation_provision ( target_table_id ); - -COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql index 61828349d..2cc8ec9e0 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql @@ -12,8 +12,7 @@ CREATE TABLE metaschema_modules_public.rls_module ( api_id uuid NOT NULL DEFAULT uuid_nil(), schema_id uuid NOT NULL DEFAULT uuid_nil(), private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), - sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), users_table_id uuid NOT NULL DEFAULT uuid_nil(), -- @@ -26,8 +25,7 @@ CREATE TABLE metaschema_modules_public.rls_module ( -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT api_fkey FOREIGN KEY (api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE, - CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT sessions_table_fkey FOREIGN KEY (sessions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT tokens_table_fkey FOREIGN KEY (tokens_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY (users_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, CONSTRAINT pschema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -41,8 +39,7 @@ COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.rls_module IS E'@ COMMENT ON CONSTRAINT pschema_fkey ON metaschema_modules_public.rls_module IS E'@omit manyToMany'; COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.rls_module IS E'@omit'; -COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; -COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; +COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.rls_module IS E'@omit'; CREATE INDEX rls_module_database_id_idx ON metaschema_modules_public.rls_module ( database_id ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql index 532d2d514..b4b360a36 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_module/table.sql @@ -7,27 +7,24 @@ BEGIN; CREATE TABLE metaschema_modules_public.table_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4 (), database_id uuid NOT NULL, - - schema_id uuid NOT NULL DEFAULT uuid_nil(), - - table_id uuid NOT NULL DEFAULT uuid_nil(), - - table_name text DEFAULT NULL, + + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + table_id uuid NOT NULL, node_type text NOT NULL, - use_rls boolean NOT NULL DEFAULT true, - data jsonb NOT NULL DEFAULT '{}', fields uuid[], + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE ); -COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; +COMMENT ON CONSTRAINT private_schema_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.table_module IS E'@omit manyToMany'; CREATE INDEX table_module_database_id_idx ON metaschema_modules_public.table_module ( database_id ); diff --git a/packages/metaschema-modules/pgpm.plan b/packages/metaschema-modules/pgpm.plan index 1c0b81bb4..4cc7e2218 100644 --- a/packages/metaschema-modules/pgpm.plan +++ b/packages/metaschema-modules/pgpm.plan @@ -32,5 +32,4 @@ schemas/metaschema_modules_public/tables/users_module/table [schemas/metaschema_ schemas/metaschema_modules_public/tables/uuid_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/uuid_module/table schemas/metaschema_modules_public/tables/hierarchy_module/table [schemas/metaschema_modules_public/schema] 2024-12-28T00:00:00Z skitch # add schemas/metaschema_modules_public/tables/hierarchy_module/table schemas/metaschema_modules_public/tables/table_template_module/table [schemas/metaschema_modules_public/schema] 2026-01-14T00:00:00Z devin # add schemas/metaschema_modules_public/tables/table_template_module/table -schemas/metaschema_modules_public/tables/secure_table_provision/table [schemas/metaschema_modules_public/schema] 2026-02-25T00:00:00Z Constructive # add schemas/metaschema_modules_public/tables/secure_table_provision/table -schemas/metaschema_modules_public/tables/relation_provision/table [schemas/metaschema_modules_public/schema] 2026-02-26T00:00:00Z Constructive # add schemas/metaschema_modules_public/tables/relation_provision/table +schemas/metaschema_modules_public/tables/secure_table_provision/table [schemas/metaschema_modules_public/schema] 2026-02-26T00:00:00Z devin # add schemas/metaschema_modules_public/tables/secure_table_provision/table diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql index 27924803a..afd1bb13e 100644 --- a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/field_module/table.sql @@ -2,6 +2,6 @@ BEGIN; -DROP TABLE IF EXISTS metaschema_modules_public.field_module; +DROP TABLE metaschema_modules_public.field_module; COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql deleted file mode 100644 index d85cc87d5..000000000 --- a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/relation_provision/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert schemas/metaschema_modules_public/tables/relation_provision/table from pg - -BEGIN; - -DROP TABLE IF EXISTS metaschema_modules_public.relation_provision; - -COMMIT; diff --git a/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql b/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql index 2f0041fef..5107a1f97 100644 --- a/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql +++ b/packages/metaschema-modules/sql/metaschema-modules--0.15.5.sql @@ -37,11 +37,11 @@ CREATE TABLE metaschema_modules_public.connected_accounts_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -80,11 +80,11 @@ CREATE TABLE metaschema_modules_public.crypto_addresses_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -113,9 +113,8 @@ CREATE TABLE metaschema_modules_public.crypto_auth_module ( database_id uuid NOT NULL, schema_id uuid NOT NULL DEFAULT uuid_nil(), users_table_id uuid NOT NULL DEFAULT uuid_nil(), + tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), - sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), - session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), addresses_table_id uuid NOT NULL DEFAULT uuid_nil(), user_field text NOT NULL, crypto_network text NOT NULL DEFAULT 'BTC', @@ -129,19 +128,15 @@ CREATE TABLE metaschema_modules_public.crypto_auth_module ( ON DELETE CASCADE, CONSTRAINT secrets_table_fkey FOREIGN KEY(secrets_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT sessions_table_fkey - FOREIGN KEY(sessions_table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT session_credentials_table_fkey - FOREIGN KEY(session_credentials_table_id) - REFERENCES metaschema_public."table" (id) + CONSTRAINT tokens_table_fkey + FOREIGN KEY(tokens_table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -155,9 +150,7 @@ COMMENT ON CONSTRAINT secrets_table_fkey ON metaschema_modules_public.crypto_aut COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; -COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; +COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.crypto_auth_module IS '@omit manyToMany'; @@ -195,11 +188,11 @@ CREATE TABLE metaschema_modules_public.denormalized_table_field ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT ref_table_fkey FOREIGN KEY(ref_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT field_fkey FOREIGN KEY(field_id) @@ -237,11 +230,11 @@ CREATE TABLE metaschema_modules_public.emails_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -281,7 +274,7 @@ CREATE TABLE metaschema_modules_public.encrypted_secrets_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -299,7 +292,6 @@ CREATE TABLE metaschema_modules_public.field_module ( private_schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), field_id uuid NOT NULL DEFAULT uuid_nil(), - node_type text NOT NULL, data jsonb NOT NULL DEFAULT '{}', triggers text[], functions text[], @@ -309,7 +301,7 @@ CREATE TABLE metaschema_modules_public.field_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT field_fkey FOREIGN KEY(field_id) @@ -331,140 +323,6 @@ COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.field_module IS '@omi CREATE INDEX field_module_database_id_idx ON metaschema_modules_public.field_module (database_id); -CREATE INDEX field_module_node_type_idx ON metaschema_modules_public.field_module (node_type); - -CREATE TABLE metaschema_modules_public.table_module ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - database_id uuid NOT NULL, - schema_id uuid NOT NULL DEFAULT uuid_nil(), - table_id uuid NOT NULL DEFAULT uuid_nil(), - table_name text DEFAULT NULL, - node_type text NOT NULL, - use_rls boolean NOT NULL DEFAULT true, - data jsonb NOT NULL DEFAULT '{}', - fields uuid[], - CONSTRAINT db_fkey - FOREIGN KEY(database_id) - REFERENCES metaschema_public.database (id) - ON DELETE CASCADE, - CONSTRAINT table_fkey - FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT schema_fkey - FOREIGN KEY(schema_id) - REFERENCES metaschema_public.schema (id) - ON DELETE CASCADE -); - -COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.table_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.table_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.table_module IS '@omit manyToMany'; - -CREATE INDEX table_module_database_id_idx ON metaschema_modules_public.table_module (database_id); - -CREATE INDEX table_module_table_id_idx ON metaschema_modules_public.table_module (table_id); - -CREATE INDEX table_module_node_type_idx ON metaschema_modules_public.table_module (node_type); - -CREATE TABLE metaschema_modules_public.secure_table_provision ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - database_id uuid NOT NULL, - schema_id uuid NOT NULL DEFAULT uuid_nil(), - table_id uuid NOT NULL DEFAULT uuid_nil(), - table_name text DEFAULT NULL, - node_type text DEFAULT NULL, - use_rls boolean NOT NULL DEFAULT true, - node_data jsonb NOT NULL DEFAULT '{}', - grant_roles text[] NOT NULL DEFAULT ARRAY['authenticated'], - grant_privileges jsonb NOT NULL DEFAULT '[]', - policy_type text DEFAULT NULL, - policy_privileges text[] DEFAULT NULL, - policy_role text DEFAULT NULL, - policy_permissive boolean NOT NULL DEFAULT true, - policy_data jsonb NOT NULL DEFAULT '{}', - out_fields uuid[] DEFAULT NULL, - CONSTRAINT db_fkey - FOREIGN KEY(database_id) - REFERENCES metaschema_public.database (id) - ON DELETE CASCADE, - CONSTRAINT table_fkey - FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT schema_fkey - FOREIGN KEY(schema_id) - REFERENCES metaschema_public.schema (id) - ON DELETE CASCADE -); - -COMMENT ON TABLE metaschema_modules_public.secure_table_provision IS 'Provisions security, fields, grants, and policies onto a table. Each row can independently: (1) create fields via node_type, (2) grant privileges via grant_privileges, (3) create RLS policies via policy_type. Multiple rows can target the same table to compose different concerns. All three concerns are optional and independent.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.id IS 'Unique identifier for this provision row.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.database_id IS 'The database this provision belongs to. Required.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.schema_id IS 'Target schema for the table. Defaults to uuid_nil(); the trigger resolves this to the app_public schema if not explicitly provided.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_id IS 'Target table to provision. Defaults to uuid_nil(); the trigger creates or resolves the table via table_name if not explicitly provided.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.table_name IS 'Name of the target table. Used to create or look up the table when table_id is not provided. If omitted, it is backfilled from the resolved table.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.node_type IS 'Which generator to invoke for field creation. One of: DataId, DataDirectOwner, DataEntityMembership, DataOwnershipInEntity, DataTimestamps, DataPeoplestamps, DataPublishable, DataSoftDelete. NULL means no field creation — the row only provisions grants and/or policies.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.use_rls IS 'If true and Row Level Security is not yet enabled on the target table, enable it. Automatically set to true by the trigger when policy_type is provided. Defaults to true.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.node_data IS 'Configuration passed to the generator function for field creation (only used when node_type is set). Known keys include: field_name (text, default ''id'') for DataId, owner_field_name (text, default ''owner_id'') for DataDirectOwner/DataOwnershipInEntity, entity_field_name (text, default ''entity_id'') for DataEntityMembership/DataOwnershipInEntity, include_id (boolean, default true) for most node_types, include_user_fk (boolean, default true) to add FK to users table. Defaults to ''{}''.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.grant_roles IS 'Database roles to grant privileges to. Supports multiple roles, e.g. ARRAY[''authenticated'', ''admin'']. Each role receives all privileges defined in grant_privileges. Defaults to ARRAY[''authenticated''].'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.grant_privileges IS 'Array of [privilege, columns] tuples defining table grants. Examples: [["select","*"],["insert","*"]] for full access, or [["update",["name","bio"]]] for column-level grants. "*" means all columns; an array means column-level grant. Defaults to ''[]'' (no grants). The trigger validates this is a proper jsonb array.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_type IS 'Policy generator type, e.g. ''AuthzEntityMembership'', ''AuthzMembership'', ''AuthzAllowAll''. NULL means no policy is created. When set, the trigger automatically enables RLS on the target table.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_privileges IS 'Privileges the policy applies to, e.g. ARRAY[''select'',''update'']. NULL means privileges are derived from the grant_privileges verbs.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_role IS 'Role the policy targets. NULL means it falls back to the first role in grant_roles.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_permissive IS 'Whether the policy is PERMISSIVE (true) or RESTRICTIVE (false). Defaults to true.'; - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.policy_data IS 'Opaque configuration passed through to metaschema.create_policy(). Structure varies by policy_type and is not interpreted by this trigger. Defaults to ''{}''.'; - - -COMMENT ON COLUMN metaschema_modules_public.secure_table_provision.out_fields IS 'Output column populated by the trigger after field creation. Contains the UUIDs of the metaschema fields created on the target table by this provision row''s generator. NULL when node_type is NULL or before the trigger runs. Callers should not set this directly.'; - -COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.secure_table_provision IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.secure_table_provision IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.secure_table_provision IS '@omit manyToMany'; - -CREATE INDEX secure_table_provision_database_id_idx ON metaschema_modules_public.secure_table_provision (database_id); - -CREATE INDEX secure_table_provision_table_id_idx ON metaschema_modules_public.secure_table_provision (table_id); - -CREATE INDEX secure_table_provision_node_type_idx ON metaschema_modules_public.secure_table_provision (node_type); - -CREATE FUNCTION metaschema_modules_private.tg_set_secure_table_provision_deterministic_id() RETURNS trigger AS $EOFCODE$ -BEGIN - IF current_setting('metaschema.deterministic_ids', true) = 'true' THEN - NEW.id := metaschema_private.deterministic_id(NEW.table_id, NEW.node_type); - END IF; - RETURN NEW; -END; -$EOFCODE$ LANGUAGE plpgsql; - -CREATE TRIGGER zzz_set_deterministic_id - BEFORE INSERT - ON metaschema_modules_public.secure_table_provision - FOR EACH ROW - EXECUTE PROCEDURE metaschema_modules_private.tg_set_secure_table_provision_deterministic_id(); - -ALTER TABLE metaschema_modules_public.secure_table_provision - ENABLE ALWAYS TRIGGER zzz_set_deterministic_id; - CREATE TABLE metaschema_modules_public.invites_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), database_id uuid NOT NULL, @@ -486,23 +344,23 @@ CREATE TABLE metaschema_modules_public.invites_module ( ON DELETE CASCADE, CONSTRAINT invites_table_fkey FOREIGN KEY(invites_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT emails_table_fkey FOREIGN KEY(emails_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT claimed_invites_table_fkey FOREIGN KEY(claimed_invites_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -571,27 +429,27 @@ CREATE TABLE metaschema_modules_public.levels_module ( ON DELETE CASCADE, CONSTRAINT steps_table_fkey FOREIGN KEY(steps_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT achievements_table_fkey FOREIGN KEY(achievements_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT levels_table_fkey FOREIGN KEY(levels_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT level_requirements_table_fkey FOREIGN KEY(level_requirements_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -644,19 +502,19 @@ CREATE TABLE metaschema_modules_public.limits_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT default_table_fkey FOREIGN KEY(default_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -690,7 +548,7 @@ CREATE TABLE metaschema_modules_public.membership_types_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -748,27 +606,27 @@ CREATE TABLE metaschema_modules_public.memberships_module ( ON DELETE CASCADE, CONSTRAINT memberships_table_fkey FOREIGN KEY(memberships_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT membership_defaults_table_fkey FOREIGN KEY(membership_defaults_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT members_table_fkey FOREIGN KEY(members_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT grants_table_fkey FOREIGN KEY(grants_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT sprt_table_fkey FOREIGN KEY(sprt_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_owner_fkey FOREIGN KEY(entity_table_owner_id) @@ -776,23 +634,23 @@ CREATE TABLE metaschema_modules_public.memberships_module ( ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT limits_table_fkey FOREIGN KEY(limits_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT default_limits_table_fkey FOREIGN KEY(default_limits_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT permissions_table_fkey FOREIGN KEY(permissions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT default_permissions_table_fkey FOREIGN KEY(default_permissions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -860,19 +718,19 @@ CREATE TABLE metaschema_modules_public.permissions_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT default_table_fkey FOREIGN KEY(default_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -904,11 +762,11 @@ CREATE TABLE metaschema_modules_public.phone_numbers_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -945,6 +803,7 @@ CREATE TABLE metaschema_modules_public.profiles_module ( profile_grants_table_name text NOT NULL DEFAULT '', profile_definition_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), profile_definition_grants_table_name text NOT NULL DEFAULT '', + bitlen int NOT NULL DEFAULT 24, membership_type int NOT NULL, entity_table_id uuid NULL, actor_table_id uuid NOT NULL DEFAULT uuid_nil(), @@ -965,35 +824,35 @@ CREATE TABLE metaschema_modules_public.profiles_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT profile_permissions_table_fkey FOREIGN KEY(profile_permissions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT profile_grants_table_fkey FOREIGN KEY(profile_grants_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT profile_definition_grants_table_fkey FOREIGN KEY(profile_definition_grants_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT actor_table_fkey FOREIGN KEY(actor_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT permissions_table_fkey FOREIGN KEY(permissions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT memberships_table_fkey FOREIGN KEY(memberships_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT profiles_module_unique UNIQUE (database_id, membership_type) @@ -1029,8 +888,7 @@ CREATE TABLE metaschema_modules_public.rls_module ( api_id uuid NOT NULL DEFAULT uuid_nil(), schema_id uuid NOT NULL DEFAULT uuid_nil(), private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), - sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), + tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), users_table_id uuid NOT NULL DEFAULT uuid_nil(), authenticate text NOT NULL DEFAULT 'authenticate', authenticate_strict text NOT NULL DEFAULT 'authenticate_strict', @@ -1044,17 +902,13 @@ CREATE TABLE metaschema_modules_public.rls_module ( FOREIGN KEY(api_id) REFERENCES services_public.apis (id) ON DELETE CASCADE, - CONSTRAINT session_credentials_table_fkey - FOREIGN KEY(session_credentials_table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT sessions_table_fkey - FOREIGN KEY(sessions_table_id) - REFERENCES metaschema_public."table" (id) + CONSTRAINT tokens_table_fkey + FOREIGN KEY(tokens_table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -1076,9 +930,7 @@ COMMENT ON CONSTRAINT pschema_fkey ON metaschema_modules_public.rls_module IS '@ COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.rls_module IS '@omit'; -COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; - -COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; +COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.rls_module IS '@omit'; @@ -1100,7 +952,7 @@ CREATE TABLE metaschema_modules_public.secrets_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -1112,18 +964,14 @@ CREATE INDEX secrets_module_database_id_idx ON metaschema_modules_public.secrets COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.secrets_module IS '@omit manyToMany'; -CREATE TABLE metaschema_modules_public.sessions_module ( +CREATE TABLE metaschema_modules_public.tokens_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), database_id uuid NOT NULL, schema_id uuid NOT NULL DEFAULT uuid_nil(), - sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), - session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), - auth_settings_table_id uuid NOT NULL DEFAULT uuid_nil(), - users_table_id uuid NOT NULL DEFAULT uuid_nil(), - sessions_default_expiration interval NOT NULL DEFAULT '30 days'::interval, - sessions_table text NOT NULL DEFAULT 'sessions', - session_credentials_table text NOT NULL DEFAULT 'session_credentials', - auth_settings_table text NOT NULL DEFAULT 'app_auth_settings', + table_id uuid NOT NULL DEFAULT uuid_nil(), + owned_table_id uuid NOT NULL DEFAULT uuid_nil(), + tokens_default_expiration interval NOT NULL DEFAULT '3 days'::interval, + tokens_table text NOT NULL DEFAULT 'api_tokens', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -1132,40 +980,25 @@ CREATE TABLE metaschema_modules_public.sessions_module ( FOREIGN KEY(schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, - CONSTRAINT sessions_table_fkey - FOREIGN KEY(sessions_table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT session_credentials_table_fkey - FOREIGN KEY(session_credentials_table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT auth_settings_table_fkey - FOREIGN KEY(auth_settings_table_id) - REFERENCES metaschema_public."table" (id) + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT users_table_fkey - FOREIGN KEY(users_table_id) - REFERENCES metaschema_public."table" (id) + CONSTRAINT owned_table_fkey + FOREIGN KEY(owned_table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); -COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.sessions_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.sessions_module IS '@omit manyToMany'; +COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; -CREATE INDEX sessions_module_database_id_idx ON metaschema_modules_public.sessions_module (database_id); +COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; -COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName sessionsTableBySessionsTableId -@omit manyToMany'; +CREATE INDEX tokens_module_database_id_idx ON metaschema_modules_public.tokens_module (database_id); -COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName sessionCredentialsTableBySessionCredentialsTableId -@omit manyToMany'; +COMMENT ON CONSTRAINT owned_table_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; -COMMENT ON CONSTRAINT auth_settings_table_fkey ON metaschema_modules_public.sessions_module IS '@fieldName authSettingsTableByAuthSettingsTableId -@omit manyToMany'; - -COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.sessions_module IS '@omit manyToMany'; +COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.tokens_module IS '@omit manyToMany'; CREATE TABLE metaschema_modules_public.user_auth_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -1175,8 +1008,7 @@ CREATE TABLE metaschema_modules_public.user_auth_module ( users_table_id uuid NOT NULL DEFAULT uuid_nil(), secrets_table_id uuid NOT NULL DEFAULT uuid_nil(), encrypted_table_id uuid NOT NULL DEFAULT uuid_nil(), - sessions_table_id uuid NOT NULL DEFAULT uuid_nil(), - session_credentials_table_id uuid NOT NULL DEFAULT uuid_nil(), + tokens_table_id uuid NOT NULL DEFAULT uuid_nil(), audits_table_id uuid NOT NULL DEFAULT uuid_nil(), audits_table_name text NOT NULL DEFAULT 'audit_logs', sign_in_function text NOT NULL DEFAULT 'sign_in', @@ -1204,27 +1036,23 @@ CREATE TABLE metaschema_modules_public.user_auth_module ( ON DELETE CASCADE, CONSTRAINT email_table_fkey FOREIGN KEY(emails_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT secrets_table_fkey FOREIGN KEY(secrets_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT encrypted_table_fkey FOREIGN KEY(encrypted_table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT sessions_table_fkey - FOREIGN KEY(sessions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT session_credentials_table_fkey - FOREIGN KEY(session_credentials_table_id) - REFERENCES metaschema_public."table" (id) + CONSTRAINT tokens_table_fkey + FOREIGN KEY(tokens_table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -1242,9 +1070,7 @@ COMMENT ON CONSTRAINT secrets_table_fkey ON metaschema_modules_public.user_auth_ COMMENT ON CONSTRAINT encrypted_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; -COMMENT ON CONSTRAINT sessions_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; - -COMMENT ON CONSTRAINT session_credentials_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; +COMMENT ON CONSTRAINT tokens_table_fkey ON metaschema_modules_public.user_auth_module IS '@omit'; CREATE TABLE metaschema_modules_public.users_module ( id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), @@ -1264,11 +1090,11 @@ CREATE TABLE metaschema_modules_public.users_module ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT type_table_fkey FOREIGN KEY(type_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -1339,23 +1165,23 @@ CREATE TABLE metaschema_modules_public.hierarchy_module ( ON DELETE CASCADE, CONSTRAINT chart_edges_table_fkey FOREIGN KEY(chart_edges_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT hierarchy_sprt_table_fkey FOREIGN KEY(hierarchy_sprt_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT chart_edge_grants_table_fkey FOREIGN KEY(chart_edge_grants_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT entity_table_fkey FOREIGN KEY(entity_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT users_table_fkey FOREIGN KEY(users_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT hierarchy_module_database_unique UNIQUE (database_id) @@ -1377,58 +1203,4 @@ COMMENT ON CONSTRAINT chart_edge_grants_table_fkey ON metaschema_modules_public. COMMENT ON CONSTRAINT entity_table_fkey ON metaschema_modules_public.hierarchy_module IS '@omit manyToMany'; -COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.hierarchy_module IS '@omit manyToMany'; - -CREATE TABLE metaschema_modules_public.table_template_module ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - database_id uuid NOT NULL, - schema_id uuid NOT NULL DEFAULT uuid_nil(), - private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - table_id uuid NOT NULL DEFAULT uuid_nil(), - owner_table_id uuid NOT NULL DEFAULT uuid_nil(), - table_name text NOT NULL, - node_type text NOT NULL, - data jsonb NOT NULL DEFAULT '{}', - CONSTRAINT db_fkey - FOREIGN KEY(database_id) - REFERENCES metaschema_public.database (id) - ON DELETE CASCADE, - CONSTRAINT table_fkey - FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT owner_table_fkey - FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE, - CONSTRAINT schema_fkey - FOREIGN KEY(schema_id) - REFERENCES metaschema_public.schema (id) - ON DELETE CASCADE, - CONSTRAINT private_schema_fkey - FOREIGN KEY(private_schema_id) - REFERENCES metaschema_public.schema (id) - ON DELETE CASCADE -); - -COMMENT ON CONSTRAINT schema_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT private_schema_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT table_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT owner_table_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; - -COMMENT ON CONSTRAINT db_fkey ON metaschema_modules_public.table_template_module IS '@omit manyToMany'; - -CREATE INDEX table_template_module_database_id_idx ON metaschema_modules_public.table_template_module (database_id); - -CREATE INDEX table_template_module_schema_id_idx ON metaschema_modules_public.table_template_module (schema_id); - -CREATE INDEX table_template_module_private_schema_id_idx ON metaschema_modules_public.table_template_module (private_schema_id); - -CREATE INDEX table_template_module_table_id_idx ON metaschema_modules_public.table_template_module (table_id); - -CREATE INDEX table_template_module_owner_table_id_idx ON metaschema_modules_public.table_template_module (owner_table_id); - -CREATE INDEX table_template_module_node_type_idx ON metaschema_modules_public.table_template_module (node_type); +COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.hierarchy_module IS '@omit manyToMany'; \ No newline at end of file diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql index 94cb11ed3..1255ee8ac 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/field_module/table.sql @@ -2,8 +2,6 @@ BEGIN; -SELECT id, database_id, private_schema_id, table_id, field_id, node_type, data, triggers, functions -FROM metaschema_modules_public.field_module -WHERE FALSE; +SELECT verify_table ('metaschema_modules_public.field_module'); ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql index 64810749e..65fa1b25c 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -6,7 +6,7 @@ SELECT id, database_id, schema_id, private_schema_id, table_id, table_name, profile_permissions_table_id, profile_permissions_table_name, profile_grants_table_id, profile_grants_table_name, profile_definition_grants_table_id, profile_definition_grants_table_name, - membership_type, entity_table_id, actor_table_id, + bitlen, membership_type, entity_table_id, actor_table_id, permissions_table_id, memberships_table_id, prefix FROM metaschema_modules_public.profiles_module WHERE FALSE; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql deleted file mode 100644 index 188f14db2..000000000 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/relation_provision/table.sql +++ /dev/null @@ -1,36 +0,0 @@ --- Verify schemas/metaschema_modules_public/tables/relation_provision/table on pg - -BEGIN; - -SELECT - id, - database_id, - relation_type, - source_table_id, - target_table_id, - field_name, - delete_action, - is_required, - junction_table_id, - junction_table_name, - junction_schema_id, - source_field_name, - target_field_name, - use_composite_key, - node_type, - node_data, - grant_roles, - grant_privileges, - policy_type, - policy_privileges, - policy_role, - policy_permissive, - policy_data, - out_field_id, - out_junction_table_id, - out_source_field_id, - out_target_field_id -FROM metaschema_modules_public.relation_provision -WHERE FALSE; - -ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql index 80cd415b0..e94672491 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_module/table.sql @@ -5,11 +5,9 @@ BEGIN; SELECT id, database_id, - schema_id, + private_schema_id, table_id, - table_name, node_type, - use_rls, data, fields FROM metaschema_modules_public.table_module diff --git a/packages/services/__tests__/__snapshots__/services.test.ts.snap b/packages/services/__tests__/__snapshots__/services.test.ts.snap new file mode 100644 index 000000000..cf59d9275 --- /dev/null +++ b/packages/services/__tests__/__snapshots__/services.test.ts.snap @@ -0,0 +1,147 @@ +// Jest Snapshot v1, https://jestjs.io/docs/snapshot-testing + +exports[`services functionality should handle complete meta workflow with services 1`] = ` +{ + "hash": null, + "id": "[ID]", + "label": null, + "name": "my-meta-db", + "owner_id": "[ID]", + "private_schema_name": null, + "schema_hash": null, + "schema_name": null, +} +`; + +exports[`services functionality should handle complete meta workflow with services 2`] = ` +{ + "anon_role": "anonymous", + "database_id": "[ID]", + "dbname": "test-database", + "id": "[ID]", + "is_public": true, + "name": "public", + "role_name": "authenticated", +} +`; + +exports[`services functionality should handle complete meta workflow with services 3`] = ` +{ + "anon_role": "administrator", + "database_id": "[ID]", + "dbname": "test-database", + "id": "[ID]", + "is_public": true, + "name": "admin", + "role_name": "administrator", +} +`; + +exports[`services functionality should handle complete meta workflow with services 4`] = ` +{ + "apple_touch_icon": null, + "database_id": "[ID]", + "dbname": "test-database", + "description": "Website Description", + "favicon": null, + "id": "[ID]", + "logo": null, + "og_image": null, + "title": "Website Title", +} +`; + +exports[`services functionality should handle complete meta workflow with services 5`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": null, + "subdomain": "api", +} +`; + +exports[`services functionality should handle complete meta workflow with services 6`] = ` +{ + "api_id": null, + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": "[ID]", + "subdomain": "app", +} +`; + +exports[`services functionality should handle complete meta workflow with services 7`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "domain": "pgpm.io", + "id": "[ID]", + "site_id": null, + "subdomain": "admin", +} +`; + +exports[`services functionality should handle complete meta workflow with services 8`] = ` +{ + "data": { + "supportEmail": "support@interweb.co", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "legal-emails", + "site_id": "[ID]", +} +`; + +exports[`services functionality should handle complete meta workflow with services 9`] = ` +{ + "api_id": "[ID]", + "data": { + "authenticate": "authenticate", + "authenticate_schema": "services_private", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "rls_module", +} +`; + +exports[`services functionality should handle complete meta workflow with services 10`] = ` +{ + "data": { + "auth_schema": "services_public", + "forgot_password": "forgot_password", + "reset_password": "reset_password", + "send_verification_email": "send_verification_email", + "set_password": "set_password", + "sign_in": "login", + "sign_up": "register", + "verify_email": "verify_email", + }, + "database_id": "[ID]", + "id": "[ID]", + "name": "user_auth_module", + "site_id": "[ID]", +} +`; + +exports[`services functionality should handle complete meta workflow with services 11`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "id": "[ID]", + "schema_id": "[ID]", +} +`; + +exports[`services functionality should handle complete meta workflow with services 12`] = ` +{ + "api_id": "[ID]", + "database_id": "[ID]", + "id": "[ID]", + "schema_id": "[ID]", +} +`;