From 3267c37868d30ff30e1c3f22d04f0812fa32186a Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 19 May 2026 21:03:55 +0000 Subject: [PATCH 1/4] feat: upstream pgpm-modules from constructive-db MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Major changes: - partman: add create_parent_with_retention procedure, update create_parent_by_id to support retention params - metaschema-modules: rename encrypted_secrets_module → config_secrets_user_module, secrets_module → user_state_module, levels_module → events_module; add config_secrets_org_module, inference_log_module, rate_limit_meters_module; remove table_template_module; update billing_module (meter_credits), entity_type_provision (invite achievements, multi-module storage), limits_module, realtime_module, storage_module - metaschema-schema: add partition columns to table, add embedding_chunks columns (search_indexes, embedding_model, embedding_provider), update node_type_registry seed (73 node types) - services: add enable_bulk column to api_settings and database_settings - object-tree: fix set_and_commit revert - object-store/object-tree: update READMEs - database-jobs: update tests for JWT claims-based db_id - all packages: add forceExit to jest configs, bump pgpm to ^4.24.4 --- packages/achievements/jest.config.js | 1 + packages/achievements/package.json | 2 +- packages/base32/jest.config.js | 1 + packages/base32/package.json | 2 +- .../__tests__/__snapshots__/jobs.test.ts.snap | 1 - packages/database-jobs/__tests__/jobs.test.ts | 35 +- packages/database-jobs/jest.config.js | 1 + packages/database-jobs/package.json | 2 +- packages/defaults/jest.config.js | 1 + packages/defaults/package.json | 2 +- .../encrypted-secrets-table/jest.config.js | 1 + packages/encrypted-secrets-table/package.json | 2 +- packages/encrypted-secrets/jest.config.js | 1 + packages/encrypted-secrets/package.json | 2 +- packages/faker/jest.config.js | 1 + packages/faker/package.json | 2 +- packages/geotypes/jest.config.js | 1 + packages/geotypes/package.json | 2 +- packages/inflection/jest.config.js | 1 + packages/inflection/package.json | 2 +- packages/jobs/jest.config.js | 1 + packages/jobs/package.json | 2 +- packages/jwt-claims/jest.config.js | 1 + packages/jwt-claims/package.json | 2 +- packages/ltree-helpers/package.json | 2 +- packages/measurements/jest.config.js | 1 + packages/measurements/package.json | 2 +- packages/metaschema-modules/README.md | 2 +- .../__tests__/modules.test.ts | 8 +- .../tables/billing_module/table.sql | 5 + .../config_secrets_org_module/table.sql | 28 + .../table.sql | 8 +- .../tables/entity_type_provision/table.sql | 77 +- .../tables/events_module/table.sql | 85 ++ .../tables/inference_log_module/table.sql | 39 + .../tables/levels_module/table.sql | 64 -- .../tables/limits_module/table.sql | 12 +- .../tables/rate_limit_meters_module/table.sql | 51 ++ .../tables/realtime_module/table.sql | 4 +- .../tables/storage_module/table.sql | 15 +- .../tables/table_template_module/table.sql | 43 - .../table.sql | 8 +- packages/metaschema-modules/jest.config.js | 1 + packages/metaschema-modules/package.json | 2 +- packages/metaschema-modules/pgpm.plan | 9 +- .../config_secrets_org_module/table.sql | 7 + .../config_secrets_user_module/table.sql | 7 + .../tables/encrypted_secrets_module/table.sql | 7 - .../tables/events_module/table.sql | 7 + .../tables/inference_log_module/table.sql | 3 + .../tables/levels_module/table.sql | 7 - .../tables/rate_limit_meters_module/table.sql | 7 + .../tables/secrets_module/table.sql | 7 - .../tables/table_template_module/table.sql | 7 - .../tables/user_state_module/table.sql | 7 + .../sql/metaschema-modules--0.15.5.sql | 520 +++++++----- .../tables/blueprint/table.sql | 5 + .../config_secrets_org_module/table.sql | 7 + .../config_secrets_user_module/table.sql | 7 + .../tables/encrypted_secrets_module/table.sql | 7 - .../tables/events_module/table.sql | 7 + .../tables/inference_log_module/table.sql | 21 + .../tables/levels_module/table.sql | 7 - .../tables/rate_limit_meters_module/table.sql | 7 + .../tables/relation_provision/table.sql | 5 +- .../tables/secrets_module/table.sql | 7 - .../tables/secure_table_provision/table.sql | 3 +- .../tables/table_template_module/table.sql | 18 - .../tables/user_state_module/table.sql | 7 + .../tables/embedding_chunks/table.sql | 8 + .../fixtures/node_type_registry_seed.sql | 496 ++++++++--- .../metaschema_public/tables/table/table.sql | 5 + packages/metaschema-schema/jest.config.js | 1 + packages/metaschema-schema/package.json | 2 +- .../sql/metaschema-schema--0.15.5.sql | 784 ++++++++++++++++-- packages/object-store/README.md | 409 ++------- packages/object-store/package.json | 2 +- packages/object-tree/README.md | 288 ++----- packages/object-tree/package.json | 2 +- .../procedures/set_and_commit.sql | 1 - packages/partman/README.md | 8 +- .../procedures/create_parent_by_id.sql | 19 +- .../create_parent_with_retention.sql | 35 + packages/partman/package.json | 2 +- packages/partman/pgpm.plan | 3 +- .../procedures/create_parent_by_id.sql | 2 +- .../create_parent_with_retention.sql | 7 + packages/partman/sql/pgpm-partman--0.0.1.sql | 54 +- .../procedures/create_parent_by_id.sql | 2 +- .../create_parent_with_retention.sql | 7 + .../tables/api_settings/table.sql | 2 + .../tables/database_settings/table.sql | 2 + packages/services/package.json | 2 +- .../tables/api_settings/table.sql | 1 + .../tables/database_settings/table.sql | 1 + packages/stamps/jest.config.js | 1 + packages/stamps/package.json | 2 +- packages/totp/jest.config.js | 1 + packages/totp/package.json | 2 +- packages/types/jest.config.js | 1 + packages/types/package.json | 2 +- packages/utils/jest.config.js | 1 + packages/utils/package.json | 2 +- packages/verify/jest.config.js | 1 + packages/verify/package.json | 2 +- 105 files changed, 2098 insertions(+), 1291 deletions(-) create mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql rename packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/{encrypted_secrets_module => config_secrets_user_module}/table.sql (63%) create mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/events_module/table.sql create mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/inference_log_module/table.sql delete mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/levels_module/table.sql create mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql delete mode 100644 packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_template_module/table.sql rename packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/{secrets_module => user_state_module}/table.sql (66%) create mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql create mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql delete mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql create mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/events_module/table.sql create mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/inference_log_module/table.sql delete mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/levels_module/table.sql create mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql delete mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/secrets_module/table.sql delete mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/table_template_module/table.sql create mode 100644 packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_state_module/table.sql create mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql create mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql delete mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql create mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql create mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/inference_log_module/table.sql delete mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/levels_module/table.sql create mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql delete mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secrets_module/table.sql delete mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_template_module/table.sql create mode 100644 packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql create mode 100644 packages/partman/deploy/schemas/partman/procedures/create_parent_with_retention.sql create mode 100644 packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql create mode 100644 packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql diff --git a/packages/achievements/jest.config.js b/packages/achievements/jest.config.js index db566b12b..4b2db888d 100644 --- a/packages/achievements/jest.config.js +++ b/packages/achievements/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', maxWorkers: 1, diff --git a/packages/achievements/package.json b/packages/achievements/package.json index b48cf2444..9abf88b43 100644 --- a/packages/achievements/package.json +++ b/packages/achievements/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/base32/jest.config.js b/packages/base32/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/base32/jest.config.js +++ b/packages/base32/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/base32/package.json b/packages/base32/package.json index 7c2bf24ae..40803b771 100644 --- a/packages/base32/package.json +++ b/packages/base32/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap b/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap index 2a9114b79..bda5397b0 100644 --- a/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap +++ b/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap @@ -2,7 +2,6 @@ exports[`scheduled jobs schedule jobs 1`] = ` { - "actor_id": null, "attempts": 0, "database_id": "5b720132-17d5-424d-9bcb-ee7b17c13d43", "id": "1", diff --git a/packages/database-jobs/__tests__/jobs.test.ts b/packages/database-jobs/__tests__/jobs.test.ts index 44297918a..453de4c04 100644 --- a/packages/database-jobs/__tests__/jobs.test.ts +++ b/packages/database-jobs/__tests__/jobs.test.ts @@ -65,20 +65,19 @@ describe('scheduled jobs', () => { const start = new Date(Date.now() + 10000); // 10s const end = new Date(start.getTime() + 180000); // +3min - // Set JWT claims for the session (required by add_scheduled_job) - await pg.any(`SELECT set_config('jwt.claims.database_id', $1, false)`, [database_id]); - const [result] = await pg.any( `SELECT * FROM app_jobs.add_scheduled_job( - identifier := $1::text, - payload := $2::json, - schedule_info := $3::json, - job_key := $4::text, - queue_name := $5::text, - max_attempts := $6::integer, - priority := $7::integer + db_id := $1::uuid, + identifier := $2::text, + payload := $3::json, + schedule_info := $4::json, + job_key := $5::text, + queue_name := $6::text, + max_attempts := $7::integer, + priority := $8::integer )`, [ + database_id, 'my_job', { just: 'run it' }, { start, end, rule: '*/1 * * * *' }, @@ -102,15 +101,17 @@ describe('scheduled jobs', () => { const [result2] = await pg.any( `SELECT * FROM app_jobs.add_scheduled_job( - identifier := $1, - payload := $2, - schedule_info := $3, - job_key := $4, - queue_name := $5, - max_attempts := $6, - priority := $7 + db_id := $1, + identifier := $2, + payload := $3, + schedule_info := $4, + job_key := $5, + queue_name := $6, + max_attempts := $7, + priority := $8 )`, [ + database_id, 'my_job', { just: 'run it' }, { start, end, rule: '*/1 * * * *' }, diff --git a/packages/database-jobs/jest.config.js b/packages/database-jobs/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/database-jobs/jest.config.js +++ b/packages/database-jobs/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/database-jobs/package.json b/packages/database-jobs/package.json index 819fb316e..6f0ca66ec 100644 --- a/packages/database-jobs/package.json +++ b/packages/database-jobs/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "dependencies": { "@pgpm/jwt-claims": "workspace:*", diff --git a/packages/defaults/jest.config.js b/packages/defaults/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/defaults/jest.config.js +++ b/packages/defaults/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/defaults/package.json b/packages/defaults/package.json index 92d4602af..4fd14d861 100644 --- a/packages/defaults/package.json +++ b/packages/defaults/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/encrypted-secrets-table/jest.config.js b/packages/encrypted-secrets-table/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/encrypted-secrets-table/jest.config.js +++ b/packages/encrypted-secrets-table/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/encrypted-secrets-table/package.json b/packages/encrypted-secrets-table/package.json index 51d4d338b..76e76883e 100644 --- a/packages/encrypted-secrets-table/package.json +++ b/packages/encrypted-secrets-table/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/encrypted-secrets/jest.config.js b/packages/encrypted-secrets/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/encrypted-secrets/jest.config.js +++ b/packages/encrypted-secrets/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/encrypted-secrets/package.json b/packages/encrypted-secrets/package.json index d8f65d338..b9ab932ef 100644 --- a/packages/encrypted-secrets/package.json +++ b/packages/encrypted-secrets/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/faker/jest.config.js b/packages/faker/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/faker/jest.config.js +++ b/packages/faker/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/faker/package.json b/packages/faker/package.json index a7a7f7afd..28d7e1805 100644 --- a/packages/faker/package.json +++ b/packages/faker/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/geotypes/jest.config.js b/packages/geotypes/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/geotypes/jest.config.js +++ b/packages/geotypes/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/geotypes/package.json b/packages/geotypes/package.json index 3a7360768..7e719d498 100644 --- a/packages/geotypes/package.json +++ b/packages/geotypes/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/inflection/jest.config.js b/packages/inflection/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/inflection/jest.config.js +++ b/packages/inflection/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/inflection/package.json b/packages/inflection/package.json index 3190daac2..06e03b7ff 100644 --- a/packages/inflection/package.json +++ b/packages/inflection/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/jobs/jest.config.js b/packages/jobs/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/jobs/jest.config.js +++ b/packages/jobs/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/jobs/package.json b/packages/jobs/package.json index 18a584891..f8443d85a 100644 --- a/packages/jobs/package.json +++ b/packages/jobs/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/jwt-claims/jest.config.js b/packages/jwt-claims/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/jwt-claims/jest.config.js +++ b/packages/jwt-claims/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/jwt-claims/package.json b/packages/jwt-claims/package.json index 604d1b1c2..813d2f158 100644 --- a/packages/jwt-claims/package.json +++ b/packages/jwt-claims/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "dependencies": { "@pgpm/types": "workspace:*", diff --git a/packages/ltree-helpers/package.json b/packages/ltree-helpers/package.json index ac5e74376..78009f4a5 100644 --- a/packages/ltree-helpers/package.json +++ b/packages/ltree-helpers/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/measurements/jest.config.js b/packages/measurements/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/measurements/jest.config.js +++ b/packages/measurements/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/measurements/package.json b/packages/measurements/package.json index a3b7e0cb4..a55ee515d 100644 --- a/packages/measurements/package.json +++ b/packages/measurements/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/metaschema-modules/README.md b/packages/metaschema-modules/README.md index d4bcbf325..535f67c89 100644 --- a/packages/metaschema-modules/README.md +++ b/packages/metaschema-modules/README.md @@ -95,7 +95,7 @@ The package provides metadata tables for the following modules: - **permissions_module**: Permission system configuration - **memberships_module**: Membership management settings - **membership_types_module**: Membership type definitions -- **levels_module**: User level configurations +- **events_module**: User level configurations ### Security - **encrypted_secrets_module**: Encrypted secrets configuration diff --git a/packages/metaschema-modules/__tests__/modules.test.ts b/packages/metaschema-modules/__tests__/modules.test.ts index 61300f8a0..70be2d69c 100644 --- a/packages/metaschema-modules/__tests__/modules.test.ts +++ b/packages/metaschema-modules/__tests__/modules.test.ts @@ -27,16 +27,16 @@ describe('db_meta_modules', () => { 'crypto_auth_module', 'default_ids_module', 'emails_module', - 'encrypted_secrets_module', + 'config_secrets_user_module', 'invites_module', - 'levels_module', + 'events_module', 'limits_module', 'membership_types_module', 'memberships_module', 'permissions_module', 'phone_numbers_module', 'rls_module', - 'secrets_module', + 'user_state_module', 'sessions_module', 'user_auth_module', 'users_module' @@ -252,4 +252,4 @@ describe('db_meta_modules', () => { })).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/billing_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_module/table.sql index ac181e493..f13957e91 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_module/table.sql @@ -27,6 +27,10 @@ CREATE TABLE metaschema_modules_public.billing_module ( balances_table_id uuid NOT NULL DEFAULT uuid_nil(), balances_table_name text NOT NULL DEFAULT '', + -- Meter credits table: append-only credit grants for billing meters + meter_credits_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_credits_table_name text NOT NULL DEFAULT '', + -- Generated functions record_usage_function text NOT NULL DEFAULT '', @@ -39,6 +43,7 @@ CREATE TABLE metaschema_modules_public.billing_module ( CONSTRAINT plan_subscriptions_table_fkey FOREIGN KEY (plan_subscriptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT ledger_table_fkey FOREIGN KEY (ledger_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT balances_table_fkey FOREIGN KEY (balances_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT meter_credits_table_fkey FOREIGN KEY (meter_credits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_module_database_id_unique UNIQUE (database_id) ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql new file mode 100644 index 000000000..dba9b6059 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql @@ -0,0 +1,28 @@ +-- Deploy schemas/metaschema_modules_public/tables/config_secrets_org_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.config_secrets_org_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'org_secrets', + -- + + 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, + CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX config_secrets_org_module_database_id_idx ON metaschema_modules_public.config_secrets_org_module ( database_id ); +CREATE INDEX config_secrets_org_module_schema_id_idx ON metaschema_modules_public.config_secrets_org_module ( schema_id ); +CREATE INDEX config_secrets_org_module_table_id_idx ON metaschema_modules_public.config_secrets_org_module ( table_id ); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_org_module IS 'Config row for the config_secrets_org_module, which provisions an organization-scoped encrypted key-value secrets store with manage_secrets permission and entity-membership RLS.'; + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql similarity index 63% rename from packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql rename to packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql index 339867857..690d3356e 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -1,17 +1,17 @@ --- Deploy schemas/metaschema_modules_public/tables/encrypted_secrets_module/table to pg +-- Deploy schemas/metaschema_modules_public/tables/config_secrets_user_module/table to pg -- requires: schemas/metaschema_modules_public/schema BEGIN; -CREATE TABLE metaschema_modules_public.encrypted_secrets_module ( +CREATE TABLE metaschema_modules_public.config_secrets_user_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), database_id uuid NOT NULL, -- schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), - table_name text NOT NULL DEFAULT 'encrypted_secrets', + table_name text NOT NULL DEFAULT 'user_secrets', -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, @@ -19,6 +19,6 @@ CREATE TABLE metaschema_modules_public.encrypted_secrets_module ( CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); -CREATE INDEX encrypted_secrets_module_database_id_idx ON metaschema_modules_public.encrypted_secrets_module ( database_id ); +CREATE INDEX config_secrets_user_module_database_id_idx ON metaschema_modules_public.config_secrets_user_module ( database_id ); COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql index 09f2a6afc..8e5f2b78e 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/entity_type_provision/table.sql @@ -51,9 +51,13 @@ CREATE TABLE metaschema_modules_public.entity_type_provision ( has_invites boolean NOT NULL DEFAULT false, + has_invite_achievements boolean NOT NULL DEFAULT false, + -- ========================================================================= - -- Storage configuration: module-level overrides + initial bucket defs. - -- Only used when has_storage = true. NULL = use defaults. + -- Storage configuration: JSON array of storage module definitions. + -- Each element provisions a separate storage module with its own tables, + -- RLS policies, and feature flags. Only used when has_storage = true. + -- NULL = provision a single default storage module with default settings. -- ========================================================================= storage_config jsonb DEFAULT NULL, @@ -197,7 +201,7 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_profiles I When true, creates profile tables and applies profiles security.'; COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_levels IS - 'Whether to provision levels_module for this type. Defaults to false. + 'Whether to provision events_module for this type. Defaults to false. Levels provide gamification/achievement tracking for members. When true, creates level steps, achievements, and level tables with security.'; @@ -216,6 +220,15 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invites IS UNIQUE (database_id, membership_type) constraint on invites_module combined with ON CONFLICT DO NOTHING in the fan-out makes repeated INSERTs safe.'; +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invite_achievements IS + 'Whether to auto-attach an EventTracker to the claimed_invites table for invite-based + achievements. Defaults to false. Requires has_invites=true AND has_levels=true. + When true, the trigger calls event_tracker() on the claimed_invites table with + event_name=''invite_claimed'', actor_field=''sender_id'', events=[''INSERT''], + crediting the SENDER (inviter) when someone claims their invite code. + Developers can then define achievements in the blueprint achievements[] section + that reference the ''invite_claimed'' event (e.g., "Invite 5 friends" = count: 5).'; + -- ============================================================================= -- Escape hatch -- ============================================================================= @@ -284,36 +297,34 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_installed_ Populated by the trigger. Useful for verifying which modules were provisioned.'; COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.storage_config IS - 'Optional jsonb object for storage module configuration and initial bucket seeding. - Only used when has_storage = true; ignored otherwise. NULL = use defaults. - Recognized keys (all optional): - - upload_url_expiry_seconds (integer) presigned PUT URL expiry override - - download_url_expiry_seconds (integer) presigned GET URL expiry override - - default_max_file_size (bigint) global max file size in bytes for this scope - - allowed_origins (text[]) default CORS origins for all buckets in this scope - - buckets (jsonb[]) array of initial bucket definitions to seed - Each bucket in the buckets array recognizes: - - name (text, required) bucket name e.g. ''documents'' - - description (text) human-readable description - - is_public (boolean) whether files are publicly readable (default false) - - allowed_mime_types (text[]) whitelist of MIME types (null = any) - - max_file_size (bigint) max file size in bytes (null = use scope default) - - allowed_origins (text[]) per-bucket CORS override - - provisions (jsonb object) optional: customize storage tables - with additional nodes, fields, grants, and policies. - Keyed by table role: "files", "buckets". - Each value uses the same shape as table_provision: - { nodes, fields, grants, use_rls, policies }. Fanned out - to secure_table_provision targeting the corresponding table. - When a key includes policies[], those REPLACE the default - storage policies for that table; tables without a key still - get defaults. Missing "data" on policy entries is auto-populated - with storage-specific defaults (same as table_provision). - Example: add SearchBm25 for full-text search on files: - {"provisions": {"files": {"nodes": [{"$type": - "SearchBm25", "data": {"source_fields": ["description"]}}]}}} - Example: - storage_config := ''{"buckets": [{"name": "documents", "is_public": false, "allowed_mime_types": ["application/pdf"]}], "provisions": {"files": {"nodes": [{"$type": "SearchBm25", "data": {"source_fields": ["description"]}}]}}}''::jsonb'; + 'Optional JSON array of storage module definitions. Each element provisions a separate + storage module with its own tables ({prefix}_{storage_key}_buckets/files), RLS policies, + and feature flags. Only used when has_storage = true; ignored otherwise. + NULL = provision a single default storage module with all defaults. + Each array element recognizes (all optional): + - storage_key (text) module discriminator, max 16 chars, lowercase snake_case. + Defaults to ''default'' (omitted from table names). + Non-default keys become infixes: {prefix}_{key}_buckets. + - upload_url_expiry_seconds (integer) presigned PUT URL expiry override + - download_url_expiry_seconds (integer) presigned GET URL expiry override + - default_max_file_size (bigint) global max file size in bytes for this module + - allowed_origins (text[]) default CORS origins for all buckets in this module + - restrict_reads (boolean) require read_files permission for SELECT on files + - has_path_shares (boolean) enable virtual filesystem + path share policies + - has_versioning (boolean) enable file version chains + - has_content_hash (boolean) enable content hash for dedup + - has_custom_keys (boolean) allow client-provided S3 keys + - has_audit_log (boolean) enable file events audit table + - has_confirm_upload (boolean) enable HeadObject confirmation flow + - confirm_upload_delay (interval) delay before first confirmation attempt + - buckets (jsonb[]) array of initial bucket definitions to seed. + Each bucket: { name (required), description, is_public, allowed_mime_types, max_file_size, allowed_origins } + - provisions (jsonb object) per-table customization keyed by "files" or "buckets". + Each value: { nodes, fields, grants, use_rls, policies }. + Example (single module, backward compat): + storage_config := ''[{"buckets": [{"name": "documents"}]}]''::jsonb + Example (multi-module): + storage_config := ''[{"has_path_shares": true, "buckets": [{"name": "documents"}]}, {"storage_key": "fn", "has_custom_keys": true, "buckets": [{"name": "functions"}]}]''::jsonb'; COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_storage_module_id IS 'Output: the UUID of the storage_module row created for this entity type. Populated by the trigger when has_storage=true.'; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/events_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/events_module/table.sql new file mode 100644 index 000000000..5e368c9a7 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/events_module/table.sql @@ -0,0 +1,85 @@ +-- Deploy schemas/metaschema_modules_public/tables/events_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.events_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + -- + + events_table_id uuid NOT NULL DEFAULT uuid_nil(), + events_table_name text NOT NULL DEFAULT '', + + event_aggregates_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_aggregates_table_name text NOT NULL DEFAULT '', + + event_types_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_types_table_name text NOT NULL DEFAULT '', + + levels_table_id uuid NOT NULL DEFAULT uuid_nil(), + levels_table_name text NOT NULL DEFAULT '', + + level_requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), + level_requirements_table_name text NOT NULL DEFAULT '', + + level_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + level_grants_table_name text NOT NULL DEFAULT '', + + achievement_rewards_table_id uuid NOT NULL DEFAULT uuid_nil(), + achievement_rewards_table_name text NOT NULL DEFAULT '', + + record_event text NOT NULL DEFAULT '', + remove_event text NOT NULL DEFAULT '', + tg_event text NOT NULL DEFAULT '', + tg_event_toggle text NOT NULL DEFAULT '', + tg_event_toggle_bool text NOT NULL DEFAULT '', + tg_event_bool text NOT NULL DEFAULT '', + upsert_aggregate text NOT NULL DEFAULT '', + tg_update_aggregates text NOT NULL DEFAULT '', + prune_events text NOT NULL DEFAULT '', + steps_required text NOT NULL DEFAULT '', + level_achieved text NOT NULL DEFAULT '', + tg_check_achievements text NOT NULL DEFAULT '', + grant_achievement text NOT NULL DEFAULT '', + tg_achievement_reward text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration for events table + "interval" text NOT NULL DEFAULT '1 month', + retention text DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, + + prefix text NULL, + + membership_type int NOT NULL, + -- if this is NOT NULL, then we add entity_id + -- e.g. limits to the app itself are considered global owned by app and no explicit owner + entity_table_id uuid NULL, + + -- required tables + actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + + + 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, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + CONSTRAINT events_table_fkey FOREIGN KEY (events_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT event_aggregates_table_fkey FOREIGN KEY (event_aggregates_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT event_types_table_fkey FOREIGN KEY (event_types_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT levels_table_fkey FOREIGN KEY (levels_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) ON DELETE CASCADE, + CONSTRAINT level_grants_table_fkey FOREIGN KEY (level_grants_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT achievement_rewards_table_fkey FOREIGN KEY (achievement_rewards_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT actor_table_fkey FOREIGN KEY (actor_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX events_module_database_id_idx ON metaschema_modules_public.events_module ( database_id ); + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/inference_log_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/inference_log_module/table.sql new file mode 100644 index 000000000..c12f26e96 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/inference_log_module/table.sql @@ -0,0 +1,39 @@ +-- Deploy schemas/metaschema_modules_public/tables/inference_log_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.inference_log_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Inference log table (partitioned by created_at) + inference_log_table_id uuid NOT NULL DEFAULT uuid_nil(), + inference_log_table_name text NOT NULL DEFAULT '', + + -- Pre-aggregated daily rollup table + usage_daily_table_id uuid NOT NULL DEFAULT uuid_nil(), + usage_daily_table_name text NOT NULL DEFAULT '', + + -- Partition lifecycle configuration + "interval" text NOT NULL DEFAULT '1 month', + retention text NULL, + premake int NOT NULL DEFAULT 2, + + prefix text NULL, + + 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, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT inference_log_table_fkey FOREIGN KEY (inference_log_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT usage_daily_table_fkey FOREIGN KEY (usage_daily_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT inference_log_module_database_id_unique UNIQUE (database_id) +); + +CREATE INDEX inference_log_module_database_id_idx ON metaschema_modules_public.inference_log_module ( database_id ); + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/levels_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/levels_module/table.sql deleted file mode 100644 index d35ae245b..000000000 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/levels_module/table.sql +++ /dev/null @@ -1,64 +0,0 @@ --- Deploy schemas/metaschema_modules_public/tables/levels_module/table to pg - --- requires: schemas/metaschema_modules_public/schema - -BEGIN; - -CREATE TABLE metaschema_modules_public.levels_module ( - id uuid PRIMARY KEY DEFAULT uuidv7(), - database_id uuid NOT NULL, - - -- - schema_id uuid NOT NULL DEFAULT uuid_nil(), - private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - -- - - steps_table_id uuid NOT NULL DEFAULT uuid_nil(), - steps_table_name text NOT NULL DEFAULT '', - - achievements_table_id uuid NOT NULL DEFAULT uuid_nil(), - achievements_table_name text NOT NULL DEFAULT '', - - levels_table_id uuid NOT NULL DEFAULT uuid_nil(), - levels_table_name text NOT NULL DEFAULT '', - - level_requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), - level_requirements_table_name text NOT NULL DEFAULT '', - - completed_step text NOT NULL DEFAULT '', - incompleted_step text NOT NULL DEFAULT '', - tg_achievement text NOT NULL DEFAULT '', - tg_achievement_toggle text NOT NULL DEFAULT '', - tg_achievement_toggle_boolean text NOT NULL DEFAULT '', - tg_achievement_boolean text NOT NULL DEFAULT '', - upsert_achievement text NOT NULL DEFAULT '', - tg_update_achievements text NOT NULL DEFAULT '', - steps_required text NOT NULL DEFAULT '', - level_achieved text NOT NULL DEFAULT '', - - prefix text NULL, - - membership_type int NOT NULL, - -- if this is NOT NULL, then we add entity_id - -- e.g. limits to the app itself are considered global owned by app and no explicit owner - entity_table_id uuid NULL, - - -- required tables - actor_table_id uuid NOT NULL DEFAULT uuid_nil(), - - - 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, - CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, - - CONSTRAINT steps_table_fkey FOREIGN KEY (steps_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT achievements_table_fkey FOREIGN KEY (achievements_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT levels_table_fkey FOREIGN KEY (levels_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) ON DELETE CASCADE, - CONSTRAINT entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT actor_table_fkey FOREIGN KEY (actor_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE -); - -CREATE INDEX user_status_module_database_id_idx ON metaschema_modules_public.levels_module ( database_id ); - -COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/limits_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/limits_module/table.sql index c101eee9e..ef7246b1f 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/limits_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/limits_module/table.sql @@ -53,6 +53,14 @@ CREATE TABLE metaschema_modules_public.limits_module ( -- Resolve cap function (COALESCE lookup: per-entity → default → 0) resolve_cap_function text NOT NULL DEFAULT '', + -- Warning tables for soft-limit notifications + limit_warnings_table_id uuid NULL, + limit_warning_state_table_id uuid NULL, + + -- Soft limit check functions + limit_check_soft_function text NOT NULL DEFAULT '', + limit_aggregate_check_soft_function text NOT NULL DEFAULT '', + prefix text NULL, membership_type int NOT NULL, @@ -77,7 +85,9 @@ CREATE TABLE metaschema_modules_public.limits_module ( CONSTRAINT credit_code_items_table_fkey FOREIGN KEY (credit_code_items_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT credit_redemptions_table_fkey FOREIGN KEY (credit_redemptions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT limit_caps_table_fkey FOREIGN KEY (limit_caps_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT limit_caps_defaults_table_fkey FOREIGN KEY (limit_caps_defaults_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE + CONSTRAINT limit_caps_defaults_table_fkey FOREIGN KEY (limit_caps_defaults_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limit_warnings_table_fkey FOREIGN KEY (limit_warnings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT limit_warning_state_table_fkey FOREIGN KEY (limit_warning_state_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql new file mode 100644 index 000000000..268a35229 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql @@ -0,0 +1,51 @@ +-- Deploy schemas/metaschema_modules_public/tables/rate_limit_meters_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.rate_limit_meters_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Public schema: rate_limit_overrides table (admin-manageable via GraphQL API) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + -- Private schema: rate_limit_state table, check_rate_limit function (internal) + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- State table: sliding window tracking per entity/actor/meter/window (private) + rate_limit_state_table_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_state_table_name text NOT NULL DEFAULT '', + + -- Overrides table: per-entity and per-actor rate limit overrides (public) + rate_limit_overrides_table_id uuid NULL, + rate_limit_overrides_table_name text NOT NULL DEFAULT '', + + -- Rate window limits table: per-plan rate limit configuration (public) + rate_window_limits_table_id uuid NULL, + rate_window_limits_table_name text NOT NULL DEFAULT '', + + -- Generated check function (private) + check_rate_limit_function text NOT NULL DEFAULT '', + + prefix text NULL, + + 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, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT rate_limit_state_table_fkey FOREIGN KEY (rate_limit_state_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT rate_limit_overrides_table_fkey FOREIGN KEY (rate_limit_overrides_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT rate_window_limits_table_fkey FOREIGN KEY (rate_window_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT rate_limit_meters_module_database_id_unique UNIQUE (database_id) +); + +CREATE INDEX rate_limit_meters_module_database_id_idx ON metaschema_modules_public.rate_limit_meters_module ( database_id ); + +COMMENT ON CONSTRAINT rate_limit_state_table_fkey + ON metaschema_modules_public.rate_limit_meters_module IS E'@fieldName rateLimitStateTableByRateLimitStateTableId'; +COMMENT ON CONSTRAINT rate_limit_overrides_table_fkey + ON metaschema_modules_public.rate_limit_meters_module IS E'@fieldName rateLimitOverridesTableByRateLimitOverridesTableId'; +COMMENT ON CONSTRAINT rate_window_limits_table_fkey + ON metaschema_modules_public.rate_limit_meters_module IS E'@fieldName rateWindowLimitsTableByRateWindowLimitsTableId'; + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/realtime_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/realtime_module/table.sql index e59367871..a521e560f 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/realtime_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/realtime_module/table.sql @@ -20,8 +20,8 @@ CREATE TABLE metaschema_modules_public.realtime_module ( -- Partition lifecycle configuration for change_log retention_hours integer NOT NULL DEFAULT 168, - lookahead_hours integer NOT NULL DEFAULT 24, - partition_interval text NOT NULL DEFAULT 'hourly', + premake int NOT NULL DEFAULT 7, + "interval" text NOT NULL DEFAULT '1 day', -- NOTIFY hybrid wake-up channel name (NULL = use default) notify_channel text NULL, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_module/table.sql index 5222d4429..7e80b5f1a 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_module/table.sql @@ -23,6 +23,12 @@ CREATE TABLE metaschema_modules_public.storage_module ( -- Multi-tenant storage identity membership_type int DEFAULT NULL, -- NULL = global gate (AuthzMembership via app_sprt), non-NULL = entity-scoped (AuthzEntityMembership) + -- Storage module discriminator: allows multiple storage modules per entity type. + -- 'default' is omitted from table names (backward compat), any other value becomes + -- an infix: {prefix}_{storage_key}_{buckets|files}. + -- Max 16 chars, lowercase snake_case, cannot be 'buckets'/'files'/'bucket'/'file'. + storage_key text NOT NULL DEFAULT 'default', + -- Configurable security policies (NULL = use defaults based on membership_type). -- When provided, replaces the default policy set in apply_storage_security. -- Accepts a JSON array of policy objects: @@ -76,6 +82,8 @@ CREATE TABLE metaschema_modules_public.storage_module ( has_content_hash boolean NOT NULL DEFAULT false, -- Content hash column for dedup + integrity verification has_custom_keys boolean NOT NULL DEFAULT false, -- allow_custom_keys on buckets (implies has_versioning + has_content_hash) has_audit_log boolean NOT NULL DEFAULT false, -- File events audit table: upload, delete, move, rename, download, share events + has_confirm_upload boolean NOT NULL DEFAULT false, -- Deferred HeadObject confirmation: enqueues storage:confirm_upload job on INSERT, creates status transition functions + confirm_upload_delay interval NOT NULL DEFAULT '30 seconds', -- Delay before first confirmation attempt (only used when has_confirm_upload = true) -- Generated table ID for file_events (populated by the generator when has_audit_log=true) file_events_table_id uuid NULL DEFAULT NULL, @@ -93,8 +101,9 @@ CREATE TABLE metaschema_modules_public.storage_module ( CREATE INDEX storage_module_database_id_idx ON metaschema_modules_public.storage_module ( database_id ); --- Unique constraint on (database_id, membership_type) using COALESCE to handle NULLs. --- NULL membership_type = app-level (only one per database), non-NULL = entity-scoped (one per membership_type per database). -CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module ( database_id, COALESCE(membership_type, -1) ); +-- Unique constraint on (database_id, membership_type, storage_key) using COALESCE to handle NULLs. +-- NULL membership_type = app-level, non-NULL = entity-scoped. storage_key discriminates +-- multiple storage modules for the same entity type (e.g. 'default' + 'fn'). +CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module ( database_id, COALESCE(membership_type, -1), storage_key ); COMMIT; 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 deleted file mode 100644 index ef126735b..000000000 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/table_template_module/table.sql +++ /dev/null @@ -1,43 +0,0 @@ --- Deploy schemas/metaschema_modules_public/tables/table_template_module/table to pg - --- requires: schemas/metaschema_modules_public/schema - -BEGIN; - -CREATE TABLE metaschema_modules_public.table_template_module ( - id uuid PRIMARY KEY DEFAULT uuidv7(), - 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 (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 '{}', - - -- - 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 -); - -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 ); - -COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/secrets_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_state_module/table.sql similarity index 66% rename from packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/secrets_module/table.sql rename to packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_state_module/table.sql index c06e2a0c3..521f5dce0 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/secrets_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_state_module/table.sql @@ -1,16 +1,16 @@ --- Deploy schemas/metaschema_modules_public/tables/secrets_module/table to pg +-- Deploy schemas/metaschema_modules_public/tables/user_state_module/table to pg -- requires: schemas/metaschema_modules_public/schema BEGIN; -CREATE TABLE metaschema_modules_public.secrets_module ( +CREATE TABLE metaschema_modules_public.user_state_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), database_id uuid NOT NULL, -- schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), - table_name text NOT NULL DEFAULT 'secrets', + table_name text NOT NULL DEFAULT 'user_state', -- 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, @@ -18,6 +18,6 @@ CREATE TABLE metaschema_modules_public.secrets_module ( ); -CREATE INDEX secrets_module_database_id_idx ON metaschema_modules_public.secrets_module ( database_id ); +CREATE INDEX user_state_module_database_id_idx ON metaschema_modules_public.user_state_module ( database_id ); COMMIT; diff --git a/packages/metaschema-modules/jest.config.js b/packages/metaschema-modules/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/metaschema-modules/jest.config.js +++ b/packages/metaschema-modules/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/metaschema-modules/package.json b/packages/metaschema-modules/package.json index 9a1c00b84..250ff34ba 100644 --- a/packages/metaschema-modules/package.json +++ b/packages/metaschema-modules/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/metaschema-modules/pgpm.plan b/packages/metaschema-modules/pgpm.plan index dcb3bff25..75d13fc21 100644 --- a/packages/metaschema-modules/pgpm.plan +++ b/packages/metaschema-modules/pgpm.plan @@ -12,9 +12,9 @@ schemas/metaschema_modules_public/tables/crypto_auth_module/table [schemas/metas schemas/metaschema_modules_public/tables/default_ids_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/default_ids_module/table schemas/metaschema_modules_public/tables/denormalized_table_field/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/denormalized_table_field/table schemas/metaschema_modules_public/tables/emails_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/emails_module/table -schemas/metaschema_modules_public/tables/encrypted_secrets_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/encrypted_secrets_module/table +schemas/metaschema_modules_public/tables/config_secrets_user_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/config_secrets_user_module/table schemas/metaschema_modules_public/tables/invites_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/invites_module/table -schemas/metaschema_modules_public/tables/levels_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/levels_module/table +schemas/metaschema_modules_public/tables/events_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/events_module/table schemas/metaschema_modules_public/tables/limits_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/limits_module/table schemas/metaschema_modules_public/tables/membership_types_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/membership_types_module/table schemas/metaschema_modules_public/tables/memberships_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/memberships_module/table @@ -22,7 +22,7 @@ schemas/metaschema_modules_public/tables/permissions_module/table [schemas/metas schemas/metaschema_modules_public/tables/phone_numbers_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/phone_numbers_module/table schemas/metaschema_modules_public/tables/profiles_module/table [schemas/metaschema_modules_public/schema] 2026-01-01T00:00:00Z devin # add schemas/metaschema_modules_public/tables/profiles_module/table schemas/metaschema_modules_public/tables/rls_module/table [schemas/metaschema_modules_public/schema schemas/services_public/tables/apis/table] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/rls_module/table -schemas/metaschema_modules_public/tables/secrets_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/secrets_module/table +schemas/metaschema_modules_public/tables/user_state_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/user_state_module/table schemas/services_public/tables/sites/table [schemas/services_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/services_public/tables/sites/table schemas/metaschema_modules_public/tables/sessions_module/table [schemas/metaschema_modules_public/schema] 2026-01-24T00:00:00Z devin # add schemas/metaschema_modules_public/tables/sessions_module/table schemas/metaschema_modules_public/tables/user_auth_module/table [schemas/metaschema_modules_public/schema] 2017-08-11T08:11:51Z skitch # add schemas/metaschema_modules_public/tables/user_auth_module/table @@ -48,3 +48,6 @@ schemas/metaschema_modules_public/tables/plans_module/table [schemas/metaschema_ schemas/metaschema_modules_public/tables/billing_module/table [schemas/metaschema_modules_public/schema] 2026-05-02T23:45:00Z devin # add billing_module config table for meters, plan_subscriptions, ledger, and balances schemas/metaschema_modules_public/tables/billing_provider_module/table [schemas/metaschema_modules_public/schema] 2026-05-03T01:00:00Z devin # add billing_provider_module config table for external billing provider integration schemas/metaschema_modules_public/tables/realtime_module/table [schemas/metaschema_modules_public/schema] 2026-05-09T10:00:00Z devin # add realtime_module config table for real-time subscription infrastructure +schemas/metaschema_modules_public/tables/rate_limit_meters_module/table [schemas/metaschema_modules_public/schema] 2026-05-16T00:00:00Z devin # add rate_limit_meters_module for rolling window abuse protection (standalone rate limiting) +schemas/metaschema_modules_public/tables/config_secrets_org_module/table [schemas/metaschema_modules_public/schema] 2026-05-18T00:00:00Z devin # add config_secrets_org_module config table for org-scoped encrypted secrets +schemas/metaschema_modules_public/tables/inference_log_module/table [schemas/metaschema_modules_public/schema] 2026-05-12T23:00:00Z devin # add inference_log_module config table for partitioned LLM inference logging diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql new file mode 100644 index 000000000..d16a1759a --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/config_secrets_org_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.config_secrets_org_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql new file mode 100644 index 000000000..fcc2139ca --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/config_secrets_user_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.config_secrets_user_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql deleted file mode 100644 index 69674ca67..000000000 --- a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert schemas/metaschema_modules_public/tables/encrypted_secrets_module/table from pg - -BEGIN; - -DROP TABLE metaschema_modules_public.encrypted_secrets_module; - -COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/events_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/events_module/table.sql new file mode 100644 index 000000000..3363a2dd8 --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/events_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/events_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.events_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/inference_log_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/inference_log_module/table.sql new file mode 100644 index 000000000..7824e3c7f --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/inference_log_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/inference_log_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.inference_log_module; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/levels_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/levels_module/table.sql deleted file mode 100644 index 46d646d6e..000000000 --- a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/levels_module/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert schemas/metaschema_modules_public/tables/levels_module/table from pg - -BEGIN; - -DROP TABLE metaschema_modules_public.levels_module; - -COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql new file mode 100644 index 000000000..b8d2b674a --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/rate_limit_meters_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.rate_limit_meters_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/secrets_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/secrets_module/table.sql deleted file mode 100644 index de31fb559..000000000 --- a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/secrets_module/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert schemas/metaschema_modules_public/tables/secrets_module/table from pg - -BEGIN; - -DROP TABLE metaschema_modules_public.secrets_module; - -COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/table_template_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/table_template_module/table.sql deleted file mode 100644 index 65aebc564..000000000 --- a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/table_template_module/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Revert schemas/metaschema_modules_public/tables/table_template_module/table from pg - -BEGIN; - -DROP TABLE IF EXISTS metaschema_modules_public.table_template_module; - -COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_state_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_state_module/table.sql new file mode 100644 index 000000000..780771724 --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_state_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_state_module/table from pg + +BEGIN; + +DROP TABLE metaschema_modules_public.user_state_module; + +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 9fc4c41cf..0c7cf0dd0 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) @@ -70,11 +70,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) @@ -109,19 +109,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 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) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -161,11 +161,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) @@ -193,11 +193,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) @@ -211,12 +211,12 @@ CREATE TABLE metaschema_modules_public.emails_module ( CREATE INDEX emails_module_database_id_idx ON metaschema_modules_public.emails_module (database_id); -CREATE TABLE metaschema_modules_public.encrypted_secrets_module ( +CREATE TABLE metaschema_modules_public.config_secrets_user_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), database_id uuid NOT NULL, schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), - table_name text NOT NULL DEFAULT 'encrypted_secrets', + table_name text NOT NULL DEFAULT 'user_secrets', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -227,11 +227,11 @@ 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 ); -CREATE INDEX encrypted_secrets_module_database_id_idx ON metaschema_modules_public.encrypted_secrets_module (database_id); +CREATE INDEX config_secrets_user_module_database_id_idx ON metaschema_modules_public.config_secrets_user_module (database_id); CREATE TABLE metaschema_modules_public.invites_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -254,23 +254,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) @@ -286,29 +286,42 @@ CREATE INDEX invites_module_database_id_idx ON metaschema_modules_public.invites CREATE UNIQUE INDEX invites_module_unique_scope ON metaschema_modules_public.invites_module (database_id, membership_type); -CREATE TABLE metaschema_modules_public.levels_module ( +CREATE TABLE metaschema_modules_public.events_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), database_id uuid NOT NULL, schema_id uuid NOT NULL DEFAULT uuid_nil(), private_schema_id uuid NOT NULL DEFAULT uuid_nil(), - steps_table_id uuid NOT NULL DEFAULT uuid_nil(), - steps_table_name text NOT NULL DEFAULT '', - achievements_table_id uuid NOT NULL DEFAULT uuid_nil(), - achievements_table_name text NOT NULL DEFAULT '', + events_table_id uuid NOT NULL DEFAULT uuid_nil(), + events_table_name text NOT NULL DEFAULT '', + event_aggregates_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_aggregates_table_name text NOT NULL DEFAULT '', + event_types_table_id uuid NOT NULL DEFAULT uuid_nil(), + event_types_table_name text NOT NULL DEFAULT '', levels_table_id uuid NOT NULL DEFAULT uuid_nil(), levels_table_name text NOT NULL DEFAULT '', level_requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), level_requirements_table_name text NOT NULL DEFAULT '', - completed_step text NOT NULL DEFAULT '', - incompleted_step text NOT NULL DEFAULT '', - tg_achievement text NOT NULL DEFAULT '', - tg_achievement_toggle text NOT NULL DEFAULT '', - tg_achievement_toggle_boolean text NOT NULL DEFAULT '', - tg_achievement_boolean text NOT NULL DEFAULT '', - upsert_achievement text NOT NULL DEFAULT '', - tg_update_achievements text NOT NULL DEFAULT '', + level_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), + level_grants_table_name text NOT NULL DEFAULT '', + achievement_rewards_table_id uuid NOT NULL DEFAULT uuid_nil(), + achievement_rewards_table_name text NOT NULL DEFAULT '', + record_event text NOT NULL DEFAULT '', + remove_event text NOT NULL DEFAULT '', + tg_event text NOT NULL DEFAULT '', + tg_event_toggle text NOT NULL DEFAULT '', + tg_event_toggle_bool text NOT NULL DEFAULT '', + tg_event_bool text NOT NULL DEFAULT '', + upsert_aggregate text NOT NULL DEFAULT '', + tg_update_aggregates text NOT NULL DEFAULT '', + prune_events text NOT NULL DEFAULT '', steps_required text NOT NULL DEFAULT '', level_achieved text NOT NULL DEFAULT '', + tg_check_achievements text NOT NULL DEFAULT '', + grant_achievement text NOT NULL DEFAULT '', + tg_achievement_reward text NOT NULL DEFAULT '', + "interval" text NOT NULL DEFAULT '1 month', + retention text DEFAULT '12 months', + premake int NOT NULL DEFAULT 2, prefix text NULL, membership_type int NOT NULL, entity_table_id uuid NULL, @@ -325,33 +338,45 @@ CREATE TABLE metaschema_modules_public.levels_module ( FOREIGN KEY(private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, - CONSTRAINT steps_table_fkey - FOREIGN KEY(steps_table_id) - REFERENCES metaschema_public."table" (id) + CONSTRAINT events_table_fkey + FOREIGN KEY(events_table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT achievements_table_fkey - FOREIGN KEY(achievements_table_id) - REFERENCES metaschema_public."table" (id) + CONSTRAINT event_aggregates_table_fkey + FOREIGN KEY(event_aggregates_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT event_types_table_fkey + FOREIGN KEY(event_types_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 level_grants_table_fkey + FOREIGN KEY(level_grants_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT achievement_rewards_table_fkey + FOREIGN KEY(achievement_rewards_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 ); -CREATE INDEX user_status_module_database_id_idx ON metaschema_modules_public.levels_module (database_id); +CREATE INDEX events_module_database_id_idx ON metaschema_modules_public.events_module (database_id); CREATE TABLE metaschema_modules_public.limits_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -378,6 +403,10 @@ CREATE TABLE metaschema_modules_public.limits_module ( limit_caps_defaults_table_id uuid NULL, cap_check_trigger text NOT NULL DEFAULT '', resolve_cap_function text NOT NULL DEFAULT '', + limit_warnings_table_id uuid NULL, + limit_warning_state_table_id uuid NULL, + limit_check_soft_function text NOT NULL DEFAULT '', + limit_aggregate_check_soft_function text NOT NULL DEFAULT '', prefix text NULL, membership_type int NOT NULL, entity_table_id uuid NULL, @@ -396,51 +425,59 @@ 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, CONSTRAINT aggregate_table_fkey FOREIGN KEY(aggregate_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT limit_credits_table_fkey FOREIGN KEY(limit_credits_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT events_table_fkey FOREIGN KEY(events_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT credit_codes_table_fkey FOREIGN KEY(credit_codes_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT credit_code_items_table_fkey FOREIGN KEY(credit_code_items_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT credit_redemptions_table_fkey FOREIGN KEY(credit_redemptions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT limit_caps_table_fkey FOREIGN KEY(limit_caps_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT limit_caps_defaults_table_fkey FOREIGN KEY(limit_caps_defaults_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT limit_warnings_table_fkey + FOREIGN KEY(limit_warnings_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT limit_warning_state_table_fkey + FOREIGN KEY(limit_warning_state_table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -462,7 +499,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 ); @@ -517,31 +554,31 @@ 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 membership_settings_table_fkey FOREIGN KEY(membership_settings_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) @@ -549,23 +586,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 ); @@ -603,19 +640,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 ); @@ -635,11 +672,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) @@ -688,39 +725,39 @@ 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 profile_templates_table_fkey FOREIGN KEY(profile_templates_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) @@ -746,15 +783,15 @@ CREATE TABLE metaschema_modules_public.rls_module ( ON DELETE CASCADE, CONSTRAINT session_credentials_table_fkey FOREIGN KEY(session_credentials_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) + 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) @@ -778,12 +815,12 @@ COMMENT ON CONSTRAINT users_table_fkey ON metaschema_modules_public.rls_module I CREATE INDEX rls_module_database_id_idx ON metaschema_modules_public.rls_module (database_id); -CREATE TABLE metaschema_modules_public.secrets_module ( +CREATE TABLE metaschema_modules_public.user_state_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), database_id uuid NOT NULL, schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), - table_name text NOT NULL DEFAULT 'secrets', + table_name text NOT NULL DEFAULT 'user_state', CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -794,11 +831,11 @@ 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 ); -CREATE INDEX secrets_module_database_id_idx ON metaschema_modules_public.secrets_module (database_id); +CREATE INDEX user_state_module_database_id_idx ON metaschema_modules_public.user_state_module (database_id); CREATE TABLE metaschema_modules_public.sessions_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -822,19 +859,19 @@ CREATE TABLE metaschema_modules_public.sessions_module ( 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) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT auth_settings_table_fkey FOREIGN KEY(auth_settings_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 ); @@ -883,27 +920,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) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT session_credentials_table_fkey FOREIGN KEY(session_credentials_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -939,11 +976,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 ); @@ -984,23 +1021,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) @@ -1026,7 +1063,7 @@ CREATE TABLE metaschema_modules_public.secure_table_provision ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -1093,11 +1130,11 @@ CREATE TABLE metaschema_modules_public.relation_provision ( ON DELETE CASCADE, CONSTRAINT source_table_fkey FOREIGN KEY(source_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT target_table_fkey FOREIGN KEY(target_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -1420,6 +1457,7 @@ CREATE TABLE metaschema_modules_public.storage_module ( buckets_table_name text NOT NULL DEFAULT 'app_buckets', files_table_name text NOT NULL DEFAULT 'app_files', membership_type int DEFAULT NULL, + storage_key text NOT NULL DEFAULT 'default', policies jsonb NULL, skip_default_policy_tables text[] NOT NULL DEFAULT '{}', entity_table_id uuid NULL, @@ -1441,6 +1479,8 @@ CREATE TABLE metaschema_modules_public.storage_module ( has_content_hash boolean NOT NULL DEFAULT false, has_custom_keys boolean NOT NULL DEFAULT false, has_audit_log boolean NOT NULL DEFAULT false, + has_confirm_upload boolean NOT NULL DEFAULT false, + confirm_upload_delay interval NOT NULL DEFAULT '30 seconds', file_events_table_id uuid NULL DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) @@ -1456,29 +1496,29 @@ CREATE TABLE metaschema_modules_public.storage_module ( ON DELETE CASCADE, CONSTRAINT buckets_table_fkey FOREIGN KEY(buckets_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT files_table_fkey FOREIGN KEY(files_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 path_shares_table_fkey FOREIGN KEY(path_shares_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT file_events_table_fkey FOREIGN KEY(file_events_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); CREATE INDEX storage_module_database_id_idx ON metaschema_modules_public.storage_module (database_id); -CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module (database_id, (COALESCE(membership_type, -1))); +CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module (database_id, (COALESCE(membership_type, -1)), storage_key); CREATE TABLE metaschema_modules_public.entity_type_provision ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -1494,6 +1534,7 @@ CREATE TABLE metaschema_modules_public.entity_type_provision ( has_levels boolean NOT NULL DEFAULT false, has_storage boolean NOT NULL DEFAULT false, has_invites boolean NOT NULL DEFAULT false, + has_invite_achievements boolean NOT NULL DEFAULT false, storage_config jsonb DEFAULT NULL, skip_entity_policies boolean NOT NULL DEFAULT false, table_provision jsonb DEFAULT NULL, @@ -1563,7 +1604,7 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_profiles I Profiles provide named permission roles (e.g. ''Editor'', ''Viewer'') with pre-configured permission bitmasks. When true, creates profile tables and applies profiles security.'; -COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_levels IS 'Whether to provision levels_module for this type. Defaults to false. +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_levels IS 'Whether to provision events_module for this type. Defaults to false. Levels provide gamification/achievement tracking for members. When true, creates level steps, achievements, and level tables with security.'; @@ -1580,6 +1621,14 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invites IS UNIQUE (database_id, membership_type) constraint on invites_module combined with ON CONFLICT DO NOTHING in the fan-out makes repeated INSERTs safe.'; +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.has_invite_achievements IS 'Whether to auto-attach an EventTracker to the claimed_invites table for invite-based + achievements. Defaults to false. Requires has_invites=true AND has_levels=true. + When true, the trigger calls event_tracker() on the claimed_invites table with + event_name=''invite_claimed'', actor_field=''sender_id'', events=[''INSERT''], + crediting the SENDER (inviter) when someone claims their invite code. + Developers can then define achievements in the blueprint achievements[] section + that reference the ''invite_claimed'' event (e.g., "Invite 5 friends" = count: 5).'; + COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.skip_entity_policies IS 'Escape hatch: when true, apply zero RLS policies to the entity table. Defaults to false. Use this only when you want the entity table provisioned with zero policies (e.g. because you plan to insert secure_table_provision rows yourself later). In most cases, prefer leaving this @@ -1633,36 +1682,34 @@ COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_entity_tab COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_installed_modules IS 'Output: array of installed module labels (e.g. ARRAY[''permissions_module:data_room'', ''memberships_module:data_room'', ''invites_module:data_room'']). Populated by the trigger. Useful for verifying which modules were provisioned.'; -COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.storage_config IS 'Optional jsonb object for storage module configuration and initial bucket seeding. - Only used when has_storage = true; ignored otherwise. NULL = use defaults. - Recognized keys (all optional): - - upload_url_expiry_seconds (integer) presigned PUT URL expiry override - - download_url_expiry_seconds (integer) presigned GET URL expiry override - - default_max_file_size (bigint) global max file size in bytes for this scope - - allowed_origins (text[]) default CORS origins for all buckets in this scope - - buckets (jsonb[]) array of initial bucket definitions to seed - Each bucket in the buckets array recognizes: - - name (text, required) bucket name e.g. ''documents'' - - description (text) human-readable description - - is_public (boolean) whether files are publicly readable (default false) - - allowed_mime_types (text[]) whitelist of MIME types (null = any) - - max_file_size (bigint) max file size in bytes (null = use scope default) - - allowed_origins (text[]) per-bucket CORS override - - provisions (jsonb object) optional: customize storage tables - with additional nodes, fields, grants, and policies. - Keyed by table role: "files", "buckets". - Each value uses the same shape as table_provision: - { nodes, fields, grants, use_rls, policies }. Fanned out - to secure_table_provision targeting the corresponding table. - When a key includes policies[], those REPLACE the default - storage policies for that table; tables without a key still - get defaults. Missing "data" on policy entries is auto-populated - with storage-specific defaults (same as table_provision). - Example: add SearchBm25 for full-text search on files: - {"provisions": {"files": {"nodes": [{"$type": - "SearchBm25", "data": {"source_fields": ["description"]}}]}}} - Example: - storage_config := ''{"buckets": [{"name": "documents", "is_public": false, "allowed_mime_types": ["application/pdf"]}], "provisions": {"files": {"nodes": [{"$type": "SearchBm25", "data": {"source_fields": ["description"]}}]}}}''::jsonb'; +COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.storage_config IS 'Optional JSON array of storage module definitions. Each element provisions a separate + storage module with its own tables ({prefix}_{storage_key}_buckets/files), RLS policies, + and feature flags. Only used when has_storage = true; ignored otherwise. + NULL = provision a single default storage module with all defaults. + Each array element recognizes (all optional): + - storage_key (text) module discriminator, max 16 chars, lowercase snake_case. + Defaults to ''default'' (omitted from table names). + Non-default keys become infixes: {prefix}_{key}_buckets. + - upload_url_expiry_seconds (integer) presigned PUT URL expiry override + - download_url_expiry_seconds (integer) presigned GET URL expiry override + - default_max_file_size (bigint) global max file size in bytes for this module + - allowed_origins (text[]) default CORS origins for all buckets in this module + - restrict_reads (boolean) require read_files permission for SELECT on files + - has_path_shares (boolean) enable virtual filesystem + path share policies + - has_versioning (boolean) enable file version chains + - has_content_hash (boolean) enable content hash for dedup + - has_custom_keys (boolean) allow client-provided S3 keys + - has_audit_log (boolean) enable file events audit table + - has_confirm_upload (boolean) enable HeadObject confirmation flow + - confirm_upload_delay (interval) delay before first confirmation attempt + - buckets (jsonb[]) array of initial bucket definitions to seed. + Each bucket: { name (required), description, is_public, allowed_mime_types, max_file_size, allowed_origins } + - provisions (jsonb object) per-table customization keyed by "files" or "buckets". + Each value: { nodes, fields, grants, use_rls, policies }. + Example (single module, backward compat): + storage_config := ''[{"buckets": [{"name": "documents"}]}]''::jsonb + Example (multi-module): + storage_config := ''[{"has_path_shares": true, "buckets": [{"name": "documents"}]}, {"storage_key": "fn", "has_custom_keys": true, "buckets": [{"name": "functions"}]}]''::jsonb'; COMMENT ON COLUMN metaschema_modules_public.entity_type_provision.out_storage_module_id IS 'Output: the UUID of the storage_module row created for this entity type. Populated by the trigger when has_storage=true.'; @@ -1694,15 +1741,15 @@ CREATE TABLE metaschema_modules_public.rate_limits_module ( ON DELETE CASCADE, CONSTRAINT rate_limit_settings_table_fkey FOREIGN KEY(rate_limit_settings_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT ip_rate_limits_table_fkey FOREIGN KEY(ip_rate_limits_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT rate_limits_table_fkey FOREIGN KEY(rate_limits_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT rate_limits_module_database_id_uniq UNIQUE (database_id) @@ -1734,11 +1781,11 @@ CREATE TABLE metaschema_modules_public.devices_module ( ON DELETE CASCADE, CONSTRAINT user_devices_table_fkey FOREIGN KEY(user_devices_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT device_settings_table_fkey FOREIGN KEY(device_settings_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT devices_module_database_id_uniq UNIQUE (database_id) @@ -1767,11 +1814,11 @@ CREATE TABLE metaschema_modules_public.session_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, CONSTRAINT sessions_table_fkey FOREIGN KEY(sessions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -1801,11 +1848,11 @@ CREATE TABLE metaschema_modules_public.webauthn_credentials_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) @@ -1850,27 +1897,27 @@ CREATE TABLE metaschema_modules_public.webauthn_auth_module ( 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 credentials_table_fkey FOREIGN KEY(credentials_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) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT session_credentials_table_fkey FOREIGN KEY(session_credentials_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT session_secrets_table_fkey FOREIGN KEY(session_secrets_table_id) - REFERENCES metaschema_public."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) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -1889,7 +1936,7 @@ CREATE TABLE metaschema_modules_public.identity_providers_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 schema_fkey FOREIGN KEY(schema_id) @@ -1937,35 +1984,35 @@ CREATE TABLE metaschema_modules_public.notifications_module ( ON DELETE CASCADE, CONSTRAINT notifications_table_fkey FOREIGN KEY(notifications_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT read_state_table_fkey FOREIGN KEY(read_state_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT preferences_table_fkey FOREIGN KEY(preferences_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT channels_table_fkey FOREIGN KEY(channels_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT delivery_log_table_fkey FOREIGN KEY(delivery_log_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT owner_table_fkey FOREIGN KEY(owner_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT user_settings_table_fkey FOREIGN KEY(user_settings_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT organization_settings_table_fkey FOREIGN KEY(organization_settings_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT schema_fkey FOREIGN KEY(schema_id) @@ -2036,19 +2083,19 @@ CREATE TABLE metaschema_modules_public.plans_module ( ON DELETE CASCADE, CONSTRAINT plans_table_fkey FOREIGN KEY(plans_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT plan_limits_table_fkey FOREIGN KEY(plan_limits_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT plan_pricing_table_fkey FOREIGN KEY(plan_pricing_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT plan_overrides_table_fkey FOREIGN KEY(plan_overrides_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT plans_module_database_id_unique UNIQUE (database_id) @@ -2069,6 +2116,8 @@ CREATE TABLE metaschema_modules_public.billing_module ( ledger_table_name text NOT NULL DEFAULT '', balances_table_id uuid NOT NULL DEFAULT uuid_nil(), balances_table_name text NOT NULL DEFAULT '', + meter_credits_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_credits_table_name text NOT NULL DEFAULT '', record_usage_function text NOT NULL DEFAULT '', prefix text NULL, CONSTRAINT db_fkey @@ -2085,19 +2134,23 @@ CREATE TABLE metaschema_modules_public.billing_module ( ON DELETE CASCADE, CONSTRAINT meters_table_fkey FOREIGN KEY(meters_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT plan_subscriptions_table_fkey FOREIGN KEY(plan_subscriptions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT ledger_table_fkey FOREIGN KEY(ledger_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT balances_table_fkey FOREIGN KEY(balances_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT meter_credits_table_fkey + FOREIGN KEY(meter_credits_table_id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_module_database_id_unique UNIQUE (database_id) @@ -2140,35 +2193,35 @@ CREATE TABLE metaschema_modules_public.billing_provider_module ( ON DELETE CASCADE, CONSTRAINT billing_customers_table_fkey FOREIGN KEY(billing_customers_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_products_table_fkey FOREIGN KEY(billing_products_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_prices_table_fkey FOREIGN KEY(billing_prices_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_subscriptions_table_fkey FOREIGN KEY(billing_subscriptions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT billing_webhook_events_table_fkey FOREIGN KEY(billing_webhook_events_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT products_table_fkey FOREIGN KEY(products_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT prices_table_fkey FOREIGN KEY(prices_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT subscriptions_table_fkey FOREIGN KEY(subscriptions_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT billing_provider_module_database_id_unique UNIQUE (database_id) @@ -2186,8 +2239,8 @@ CREATE TABLE metaschema_modules_public.realtime_module ( listener_node_table_id uuid NOT NULL DEFAULT uuid_nil(), source_registry_table_id uuid NOT NULL DEFAULT uuid_nil(), retention_hours int NOT NULL DEFAULT 168, - lookahead_hours int NOT NULL DEFAULT 24, - partition_interval text NOT NULL DEFAULT 'hourly', + premake int NOT NULL DEFAULT 7, + "interval" text NOT NULL DEFAULT '1 day', notify_channel text NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) @@ -2207,15 +2260,15 @@ CREATE TABLE metaschema_modules_public.realtime_module ( ON DELETE CASCADE, CONSTRAINT change_log_table_fkey FOREIGN KEY(change_log_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT listener_node_table_fkey FOREIGN KEY(listener_node_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT source_registry_table_fkey FOREIGN KEY(source_registry_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -2233,4 +2286,81 @@ CREATE INDEX realtime_module_change_log_table_id_idx ON metaschema_modules_publi CREATE INDEX realtime_module_listener_node_table_id_idx ON metaschema_modules_public.realtime_module (listener_node_table_id); -CREATE INDEX realtime_module_source_registry_table_id_idx ON metaschema_modules_public.realtime_module (source_registry_table_id); \ No newline at end of file +CREATE INDEX realtime_module_source_registry_table_id_idx ON metaschema_modules_public.realtime_module (source_registry_table_id); + +CREATE TABLE metaschema_modules_public.rate_limit_meters_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_state_table_id uuid NOT NULL DEFAULT uuid_nil(), + rate_limit_state_table_name text NOT NULL DEFAULT '', + rate_limit_overrides_table_id uuid NULL, + rate_limit_overrides_table_name text NOT NULL DEFAULT '', + rate_window_limits_table_id uuid NULL, + rate_window_limits_table_name text NOT NULL DEFAULT '', + check_rate_limit_function text NOT NULL DEFAULT '', + prefix text NULL, + 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, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT rate_limit_state_table_fkey + FOREIGN KEY(rate_limit_state_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT rate_limit_overrides_table_fkey + FOREIGN KEY(rate_limit_overrides_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT rate_window_limits_table_fkey + FOREIGN KEY(rate_window_limits_table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE, + CONSTRAINT rate_limit_meters_module_database_id_unique + UNIQUE (database_id) +); + +CREATE INDEX rate_limit_meters_module_database_id_idx ON metaschema_modules_public.rate_limit_meters_module (database_id); + +COMMENT ON CONSTRAINT rate_limit_state_table_fkey ON metaschema_modules_public.rate_limit_meters_module IS '@fieldName rateLimitStateTableByRateLimitStateTableId'; + +COMMENT ON CONSTRAINT rate_limit_overrides_table_fkey ON metaschema_modules_public.rate_limit_meters_module IS '@fieldName rateLimitOverridesTableByRateLimitOverridesTableId'; + +COMMENT ON CONSTRAINT rate_window_limits_table_fkey ON metaschema_modules_public.rate_limit_meters_module IS '@fieldName rateWindowLimitsTableByRateWindowLimitsTableId'; + +CREATE TABLE metaschema_modules_public.config_secrets_org_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL DEFAULT uuid_nil(), + table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'org_secrets', + 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, + CONSTRAINT table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public.table (id) + ON DELETE CASCADE +); + +CREATE INDEX config_secrets_org_module_database_id_idx ON metaschema_modules_public.config_secrets_org_module (database_id); + +CREATE INDEX config_secrets_org_module_schema_id_idx ON metaschema_modules_public.config_secrets_org_module (schema_id); + +CREATE INDEX config_secrets_org_module_table_id_idx ON metaschema_modules_public.config_secrets_org_module (table_id); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_org_module IS 'Config row for the config_secrets_org_module, which provisions an organization-scoped encrypted key-value secrets store with manage_secrets permission and entity-membership RLS.'; \ No newline at end of file diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql index d5683b397..7a71abb5b 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql @@ -11,6 +11,11 @@ SELECT description, definition, template_id, + status, + constructed_at, + error_details, + ref_map, + constructed_definition, definition_hash, table_hashes, created_at, diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql new file mode 100644 index 000000000..b85512867 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_org_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/config_secrets_org_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.config_secrets_org_module'); + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql new file mode 100644 index 000000000..6ca9dcfd0 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/config_secrets_user_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.config_secrets_user_module'); + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql deleted file mode 100644 index a2780bcc6..000000000 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/encrypted_secrets_module/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify schemas/metaschema_modules_public/tables/encrypted_secrets_module/table on pg - -BEGIN; - -SELECT verify_table ('metaschema_modules_public.encrypted_secrets_module'); - -ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql new file mode 100644 index 000000000..fcc1bd224 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/events_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/events_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.events_module'); + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/inference_log_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/inference_log_module/table.sql new file mode 100644 index 000000000..a5c1c2aa4 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/inference_log_module/table.sql @@ -0,0 +1,21 @@ +-- Verify schemas/metaschema_modules_public/tables/inference_log_module/table on pg + +BEGIN; + +SELECT + id, + database_id, + schema_id, + private_schema_id, + inference_log_table_id, + inference_log_table_name, + usage_daily_table_id, + usage_daily_table_name, + "interval", + retention, + premake, + prefix +FROM metaschema_modules_public.inference_log_module +WHERE FALSE; + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/levels_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/levels_module/table.sql deleted file mode 100644 index e80305133..000000000 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/levels_module/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify schemas/metaschema_modules_public/tables/levels_module/table on pg - -BEGIN; - -SELECT verify_table ('metaschema_modules_public.levels_module'); - -ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql new file mode 100644 index 000000000..1053f4f62 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/rate_limit_meters_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/rate_limit_meters_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.rate_limit_meters_module'); + +ROLLBACK; 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 index 0caae8560..d6bf096f3 100644 --- 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 @@ -18,9 +18,8 @@ SELECT source_field_name, target_field_name, use_composite_key, - create_index, - expose_in_api, - nodes, + node_type, + node_data, grants, policies, out_field_id, diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secrets_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secrets_module/table.sql deleted file mode 100644 index 4d201804c..000000000 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secrets_module/table.sql +++ /dev/null @@ -1,7 +0,0 @@ --- Verify schemas/metaschema_modules_public/tables/secrets_module/table on pg - -BEGIN; - -SELECT verify_table ('metaschema_modules_public.secrets_module'); - -ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql index f953e5071..b22b45203 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql @@ -8,8 +8,9 @@ SELECT schema_id, table_id, table_name, - nodes, + node_type, use_rls, + node_data, fields, grants, policies, diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_template_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_template_module/table.sql deleted file mode 100644 index c1d7dd59a..000000000 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/table_template_module/table.sql +++ /dev/null @@ -1,18 +0,0 @@ --- Verify schemas/metaschema_modules_public/tables/table_template_module/table on pg - -BEGIN; - -SELECT - id, - database_id, - schema_id, - private_schema_id, - table_id, - owner_table_id, - table_name, - node_type, - data -FROM metaschema_modules_public.table_template_module -WHERE FALSE; - -ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql new file mode 100644 index 000000000..36655782d --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_state_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_state_module/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_modules_public.user_state_module'); + +ROLLBACK; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/embedding_chunks/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/embedding_chunks/table.sql index 8f7e541c9..2667d2139 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/embedding_chunks/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/embedding_chunks/table.sql @@ -31,10 +31,18 @@ CREATE TABLE metaschema_public.embedding_chunks ( -- metadata fields from parent to copy into chunks metadata_fields jsonb, + -- search indexes to create on the chunks content column + -- NULL means "mirror parent table's text search indexes" + search_indexes jsonb, + -- job configuration enqueue_chunking_job boolean NOT NULL DEFAULT true, chunking_task_name text NOT NULL DEFAULT 'generate_chunks', + -- model config (optional — worker falls back to runtime config when null) + embedding_model text, + embedding_provider text, + -- FK field on chunks table pointing to parent parent_fk_field_id uuid, diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql index 1f825d849..11dbce35c 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -3,7 +3,7 @@ -- GENERATED FILE — DO NOT EDIT -- Regenerate with: cd packages/node-type-registry && pnpm generate -- --- Node types: 61 +-- Node types: 73 -- requires: schemas/metaschema_public/schema -- requires: schemas/metaschema_public/tables/node_type_registry/table @@ -426,13 +426,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataChunks', - 'data_chunks', - 'data', - 'Chunks', - 'Creates a chunked-embedding child table for any parent table. Provisions the chunks table with content, chunk_index, embedding vector, metadata, HNSW index, inherited RLS, and optional job trigger for automatic text splitting. Composed internally by DataFileEmbedding (enabled by default in extract mode) but can also be used standalone.', - '{"type":"object","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"dimensions":{"type":"integer","description":"Vector dimensions for per-chunk embeddings","default":768},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric for the HNSW index on chunk embeddings","default":"cosine"},"chunks_table_name":{"type":"string","description":"Override the chunks table name. Defaults to {parent_table}_chunks."},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from the parent table to copy into chunk metadata"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to create a job trigger that auto-enqueues chunking on parent INSERT/UPDATE","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}'::jsonb, - '{"embedding","chunks","vector","ai","rag"}'::text[] + 'LimitTrackUsage', + 'limit_track_usage', + 'limit_track', + 'Track Usage', + 'Declaratively attaches billing usage-recording triggers to a table. On INSERT the named meter is incremented via record_usage; on DELETE it is decremented (reversal). On UPDATE, if the entity_field changes, the old entity is decremented and the new entity is incremented. Requires a provisioned billing_module for the target database.', + '{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to record usage against (must match a meters table entry, e.g. \"databases\", \"seats\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for billing","default":"entity_id"},"quantity":{"type":"integer","description":"Units to record per event (default 1)","default":1},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["meter_slug"]}'::jsonb, + '{"billing","triggers","metering","usage","track"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -450,13 +450,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataCompositeField', - 'data_composite_field', - 'data', - 'Composite Field', - 'Creates a derived text field that automatically concatenates multiple source fields via BEFORE INSERT/UPDATE triggers. Used to produce a unified text representation (e.g., embedding_text) from multiple columns on a table. The trigger fires with ''_000'' prefix to run before Search* triggers alphabetically.', - '{"type":"object","properties":{"target":{"type":"string","format":"column-ref","description":"Name of the derived text field to create (default: ''embedding_text'')"},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of source field names to concatenate into the target field"},"format":{"type":"string","enum":["labeled","plain"],"description":"Output format: ''labeled'' (field_name: value) or ''plain'' (values only). Default: ''labeled''"}},"required":["source_fields"]}'::jsonb, - '{"transform","behavior"}'::text[] + 'CheckGreaterThan', + 'check_greater_than', + 'check', + 'Check Greater Than', + 'Adds a CHECK constraint that validates a column value is greater than a threshold (single-column: column > value) or that one column is greater than another (cross-column: columns[0] > columns[1]). Compiled via AST helpers.', + '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column > value)","default":0},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] > columns[1])","minItems":2,"maxItems":2}}}'::jsonb, + '{"check","constraint","validation","comparison"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -474,13 +474,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataDirectOwner', - 'data_direct_owner', - 'data', - 'Ownership', - 'Adds ownership column for direct user ownership. Enables AuthzDirectOwner authorization.', - '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for owner ID","default":"owner_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from owner_id to the users table","default":true}}}'::jsonb, - '{"ownership","schema"}'::text[] + 'CheckLessThan', + 'check_less_than', + 'check', + 'Check Less Than', + 'Adds a CHECK constraint that validates a column value is less than a threshold (single-column: column < value) or that one column is less than another (cross-column: columns[0] < columns[1]). Compiled via AST helpers.', + '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column < value)"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] < columns[1])","minItems":2,"maxItems":2}}}'::jsonb, + '{"check","constraint","validation","comparison"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -498,13 +498,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataEntityMembership', - 'data_entity_membership', - 'data', - 'Entity Membership', - 'Adds entity reference for organization/group scoping. Enables AuthzEntityMembership, AuthzMembership, AuthzOrgHierarchy authorization.', - '{"type":"object","properties":{"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for entity ID","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from entity_id to the users table","default":true}}}'::jsonb, - '{"membership","schema"}'::text[] + 'CheckNotEqual', + 'check_not_equal', + 'check', + 'Check Not Equal', + 'Adds a CHECK constraint that validates two columns are not equal (columns[0] != columns[1]). Useful for preventing self-referencing rows. Compiled via AST helpers.', + '{"type":"object","properties":{"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns that must not be equal","minItems":2,"maxItems":2}},"required":["columns"]}'::jsonb, + '{"check","constraint","validation","inequality"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -522,13 +522,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataFeatureFlag', - 'data_feature_flag', - 'data', - 'Feature Flag', - 'Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0).', - '{"type":"object","properties":{"feature_name":{"type":"string","description":"Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1)"},"scope":{"type":"string","enum":["app","org"],"description":"Feature scope: \"app\" (membership_type=1, app-level caps) or \"org\" (membership_type=2, per-entity caps)","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope)","default":"entity_id"}},"required":["feature_name"]}'::jsonb, - '{"limits","triggers","feature-flags","billing","caps"}'::text[] + 'CheckOneOf', + 'check_one_of', + 'check', + 'Check One Of', + 'Adds a CHECK constraint that validates a column value is one of an allowed set (e.g. tier IN (''free'', ''paid'', ''custom'')). Compiled to column = ANY(ARRAY[...]) via AST helpers.', + '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Column to validate against the allowed values"},"values":{"type":"array","items":{"type":"string"},"description":"Array of allowed values for the column"}},"required":["column","values"]}'::jsonb, + '{"check","constraint","validation","enum"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -546,13 +546,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataFileEmbedding', - 'data_file_embedding', + 'DataBulk', + 'data_bulk', 'data', - 'File Embedding', - 'Generic, MIME-scoped embedding node for file tables. Supports two modes: direct (whole-file to single vector, e.g. CLIP for images) when extraction is omitted, or extract (file to text to chunks to per-chunk vectors) when extraction config is provided. Composes SearchVector + DataJobTrigger + DataChunks (enabled by default in extract mode) internally. Multiple instances can coexist on the same table with different MIME scopes, field names, and embedding strategies.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 512 for CLIP, 768 for nomic, 1536 for ada-002)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''image/%''], [''application/pdf'', ''text/%''], [''audio/%''].","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker.","default":"process_file_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks, field guards, etc.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"extraction":{"type":"object","description":"Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"},"status_field":{"type":"string","format":"column-ref","description":"Extraction lifecycle status field","default":"extraction_status"}}},"include_chunks":{"type":"boolean","description":"Whether to create a chunks table via DataChunks. Defaults to true when extraction is provided, false in direct mode. Set explicitly to override."},"chunks":{"type":"object","description":"Chunking configuration passed through to DataChunks. When include_chunks is true (or defaults to true in extract mode), these params configure the chunks table, embedding dimensions, strategy, etc.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from parent to copy into chunk metadata"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"stale_strategy":{"type":"string","enum":["column","null","hash"],"description":"Strategy for tracking embedding staleness when extraction is enabled. column: embedding_stale boolean. null: set embedding to NULL. hash: md5 hash.","default":"column"},"include_stale_field":{"type":"boolean","description":"Whether to include the embedding_stale boolean field (extract mode)","default":true}}}'::jsonb, - '{"embedding","vector","ai","composition","jobs","multimodal","files"}'::text[] + 'Bulk Operations', + 'Enables bulk mutation smart tags on a table. When provisioned, adds @behavior tags for the selected bulk operations (insert, upsert, update, delete). Requires the graphile-bulk-mutations plugin.', + '{"type":"object","properties":{"insert":{"type":"boolean","description":"Enable bulk insert (+bulkInsert)","default":true},"upsert":{"type":"boolean","description":"Enable bulk upsert (+bulkUpsert)","default":false},"update":{"type":"boolean","description":"Enable bulk update (+bulkUpdate)","default":false},"delete":{"type":"boolean","description":"Enable bulk delete (+bulkDelete)","default":false}}}'::jsonb, + '{"bulk","mutations","graphile"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -570,13 +570,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataForceCurrentUser', - 'data_force_current_user', + 'DataCompositeField', + 'data_composite_field', 'data', - 'Force Current User', - 'BEFORE INSERT trigger that forces a field to the value of jwt_public.current_user_id(). Prevents clients from spoofing the actor/uploader identity. The field value is always overwritten regardless of what the client provides.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to force to current_user_id()","default":"actor_id"}}}'::jsonb, - '{"trigger","security","schema"}'::text[] + 'Composite Field', + 'Creates a derived text field that automatically concatenates multiple source fields via BEFORE INSERT/UPDATE triggers. Used to produce a unified text representation (e.g., embedding_text) from multiple columns on a table. The trigger fires with ''_000'' prefix to run before Search* triggers alphabetically.', + '{"type":"object","properties":{"target":{"type":"string","format":"column-ref","description":"Name of the derived text field to create","default":"embedding_text"},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of source field names to concatenate into the target field"},"format":{"type":"string","enum":["labeled","plain"],"description":"Output format: ''labeled'' (field_name: value) or ''plain'' (values only)","default":"labeled"}},"required":["source_fields"]}'::jsonb, + '{"transform","behavior"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -594,13 +594,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataId', - 'data_id', + 'DataDirectOwner', + 'data_direct_owner', 'data', - 'Primary Key ID', - 'Adds a UUID primary key column with auto-generation default (uuidv7). This is the standard primary key pattern for all tables.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the primary key","default":"id"}}}'::jsonb, - '{"primary_key","schema"}'::text[] + 'Ownership', + 'Adds ownership column for direct user ownership. Enables AuthzDirectOwner authorization.', + '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for owner ID","default":"owner_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from owner_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the owner column","default":true}}}'::jsonb, + '{"ownership","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -618,13 +618,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataImageEmbedding', - 'data_image_embedding', + 'DataEntityMembership', + 'data_entity_membership', 'data', - 'Image Embedding', - 'Image-specific preset of DataFileEmbedding. Delegates to DataFileEmbedding with image-oriented defaults: dimensions=512 (CLIP), mime_patterns=[''image/%''], task_identifier=''process_image_embedding'', direct mode (no extraction). Accepts all DataFileEmbedding parameters — any overrides are forwarded through.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (default 512 for CLIP-style image embeddings)","default":512},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image embedding worker","default":"process_image_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"extraction":{"type":"object","description":"Text extraction configuration. Forwarded to DataFileEmbedding. When present, enables extract mode (e.g., OCR for images).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata","default":"extracted_metadata"},"status_field":{"type":"string","format":"column-ref","description":"Extraction lifecycle status field","default":"extraction_status"}}},"chunks":{"type":"object","description":"Chunking configuration. Forwarded to DataFileEmbedding. Only meaningful when extraction is also provided.","properties":{"content_field_name":{"type":"string","format":"column-ref","default":"content"},"chunk_size":{"type":"integer","default":1000},"chunk_overlap":{"type":"integer","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"default":"paragraph"},"metadata_fields":{"type":"object"},"enqueue_chunking_job":{"type":"boolean","default":true},"chunking_task_name":{"type":"string","default":"generate_chunks"}}},"stale_strategy":{"type":"string","enum":["column","null","hash"],"description":"Strategy for tracking embedding staleness in extract mode","default":"column"},"include_stale_field":{"type":"boolean","description":"Whether to include the embedding_stale boolean field","default":true}}}'::jsonb, - '{"embedding","image","vector","ai","composition","jobs"}'::text[] + 'Entity Membership', + 'Adds entity reference for organization/group scoping. Enables AuthzEntityMembership, AuthzMembership, AuthzOrgHierarchy authorization.', + '{"type":"object","properties":{"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for entity ID","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the entity column","default":true}}}'::jsonb, + '{"membership","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -642,13 +642,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataImmutableFields', - 'data_immutable_fields', + 'DataForceCurrentUser', + 'data_force_current_user', 'data', - 'Immutable Fields', - 'BEFORE UPDATE trigger that prevents changes to a list of specified fields after INSERT. Raises an exception if any of the listed fields have changed. Unlike FieldImmutable (single-field), this handles multiple fields in a single trigger for efficiency.', - '{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names that cannot be modified after INSERT (e.g. [\"key\", \"bucket_id\", \"owner_id\"])"}},"required":["fields"]}'::jsonb, - '{"trigger","constraint","schema"}'::text[] + 'Force Current User', + 'BEFORE INSERT trigger that forces a field to the value of jwt_public.current_user_id(). Prevents clients from spoofing the actor/uploader identity. The field value is always overwritten regardless of what the client provides.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to force to current_user_id()","default":"actor_id"}}}'::jsonb, + '{"trigger","security","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -666,13 +666,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataInflection', - 'data_inflection', + 'DataId', + 'data_id', 'data', - 'Inflection', - 'Transforms field values using inflection operations (snake_case, camelCase, slugify, plural, singular, etc). Attaches BEFORE INSERT and BEFORE UPDATE triggers. References fields by name in data jsonb.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to transform"},"ops":{"type":"array","items":{"type":"string","enum":["plural","singular","camel","pascal","dashed","slugify","underscore","lower","upper"]},"description":"Inflection operations to apply in order"}},"required":["field_name","ops"]}'::jsonb, - '{"transform","behavior"}'::text[] + 'Primary Key ID', + 'Adds a UUID primary key column with auto-generation default (uuidv7). This is the standard primary key pattern for all tables.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the primary key","default":"id"}}}'::jsonb, + '{"primary_key","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -690,13 +690,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataInheritFromParent', - 'data_inherit_from_parent', + 'DataImmutableFields', + 'data_immutable_fields', 'data', - 'Inherit From Parent', - 'BEFORE INSERT trigger that copies specified fields from a parent table via a foreign key. The parent row is looked up through RLS (SECURITY INVOKER), so the insert fails if the caller cannot see the parent. Used by the storage module to inherit owner_id and is_public from buckets to files.', - '{"type":"object","properties":{"parent_fk_field":{"type":"string","format":"column-ref","description":"Name of the FK field on this table that references the parent (e.g. bucket_id)"},"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to copy from the parent row (e.g. [\"owner_id\", \"is_public\"])"},"parent_table":{"type":"string","description":"Parent table name (optional fallback if FK not yet registered in metaschema)"},"parent_schema":{"type":"string","description":"Parent table schema (optional, defaults to same schema as child table)"}},"required":["parent_fk_field","fields"]}'::jsonb, - '{"trigger","inheritance","schema"}'::text[] + 'Immutable Fields', + 'BEFORE UPDATE trigger that prevents changes to a list of specified fields after INSERT. Raises an exception if any of the listed fields have changed. Unlike FieldImmutable (single-field), this handles multiple fields in a single trigger for efficiency.', + '{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names that cannot be modified after INSERT (e.g. [\"key\", \"bucket_id\", \"owner_id\"])"}},"required":["fields"]}'::jsonb, + '{"trigger","constraint","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -714,13 +714,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataJobTrigger', - 'data_job_trigger', + 'DataInflection', + 'data_inflection', 'data', - 'Job Trigger', - 'Dynamically creates PostgreSQL triggers that enqueue jobs via app_jobs.add_job() when table rows are inserted, updated, or deleted. Supports configurable payload strategies (full row, row ID, selected fields, or custom mapping), conditional firing via WHEN clauses, watched field changes, and extended job options (queue, priority, delay, max attempts).', - '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"task_identifier":{"type":"string","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe)"},"payload_strategy":{"type":"string","enum":["row","row_id","fields","custom"],"description":"How to build the job payload: row (full NEW/OLD), row_id (just id), fields (selected columns), custom (mapped columns)","default":"row_id"},"payload_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names to include in payload (only for fields strategy)"},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Key-to-column mapping for custom payload (e.g., {\"invoice_id\": \"id\", \"total\": \"amount\"})"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Trigger events to create","default":["INSERT","UPDATE"]},"include_old":{"type":"boolean","description":"Include OLD row in payload (for UPDATE triggers)","default":false},"include_meta":{"type":"boolean","description":"Include table/schema metadata in payload","default":false},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"},"job_key":{"type":"string","description":"Static job key for upsert semantics (prevents duplicate jobs)"},"queue_name":{"type":"string","description":"Job queue name for routing to specific workers"},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"run_at_delay":{"type":"string","description":"Delay before job runs as PostgreSQL interval (e.g., 30 seconds, 5 minutes)"},"max_attempts":{"type":"integer","description":"Maximum retry attempts for the job","default":25}},"required":["task_identifier"]}'::jsonb, - '{"jobs","triggers","async"}'::text[] + 'Inflection', + 'Transforms field values using inflection operations (snake_case, camelCase, slugify, plural, singular, etc). Attaches BEFORE INSERT and BEFORE UPDATE triggers. References fields by name in data jsonb.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to transform"},"ops":{"type":"array","items":{"type":"string","enum":["plural","singular","camel","pascal","dashed","slugify","underscore","lower","upper"]},"description":"Inflection operations to apply in order"}},"required":["field_name","ops"]}'::jsonb, + '{"transform","behavior"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -738,13 +738,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataJsonb', - 'data_jsonb', + 'DataInheritFromParent', + 'data_inherit_from_parent', 'data', - 'JSONB Field', - 'Adds a JSONB column with optional GIN index for containment queries (@>, ?, ?|, ?&). Standard pattern for semi-structured metadata.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the JSONB field","default":"metadata"},"default_value":{"type":"string","description":"Default value expression"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false},"create_index":{"type":"boolean","description":"Whether to create a GIN index","default":true}}}'::jsonb, - '{"jsonb","schema"}'::text[] + 'Inherit From Parent', + 'BEFORE INSERT trigger that copies specified fields from a parent table via a foreign key. The parent row is looked up through RLS (SECURITY INVOKER), so the insert fails if the caller cannot see the parent. Used by the storage module to inherit owner_id and is_public from buckets to files.', + '{"type":"object","properties":{"parent_fk_field":{"type":"string","format":"column-ref","description":"Name of the FK field on this table that references the parent (e.g. bucket_id)"},"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to copy from the parent row (e.g. [\"owner_id\", \"is_public\"])"},"parent_table":{"type":"string","description":"Parent table name (optional fallback if FK not yet registered in metaschema)"},"parent_schema":{"type":"string","description":"Parent table schema (optional, defaults to same schema as child table)"}},"required":["parent_fk_field","fields"]}'::jsonb, + '{"trigger","inheritance","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -762,13 +762,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES ( - 'DataLimitCounter', - 'data_limit_counter', + 'DataJsonb', + 'data_jsonb', 'data', - 'Limit Counter', - 'Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope.', - '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to track (must match a default_limits entry, e.g. \"projects\", \"members\")"},"scope":{"type":"string","enum":["app","org"],"description":"Limit scope: \"app\" (membership_type=1, user-level) or \"org\" (membership_type=2, entity-level)","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor or entity id used for limit lookup","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}'::jsonb, - '{"limits","triggers","billing"}'::text[] + 'JSONB Field', + 'Adds a JSONB column with optional GIN index for containment queries (@>, ?, ?|, ?&). Standard pattern for semi-structured metadata.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the JSONB field","default":"metadata"},"default_value":{"type":"string","description":"Default value expression","default":"''{}''::jsonb"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false},"create_index":{"type":"boolean","description":"Whether to create a GIN index","default":true}}}'::jsonb, + '{"jsonb","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, category = EXCLUDED.category, @@ -815,7 +815,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'data', 'Ownership In Entity', 'Combines direct ownership with entity scoping. Adds both owner_id and entity_id columns. Enables AuthzDirectOwner, AuthzEntityMembership, and AuthzOrgHierarchy authorization. Particularly useful for OrgHierarchy where a user owns a row (owner_id) within an entity (entity_id), and managers above can see subordinate-owned records via the hierarchy closure table.', - '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true}}}'::jsonb, + '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the owner and entity columns","default":true}}}'::jsonb, '{"ownership","membership","hierarchy","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -839,7 +839,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'data', 'Peoplestamps', 'Adds user tracking for creates/updates with created_by and updated_by columns.', - '{"type":"object","properties":{"created_by_field":{"type":"string","format":"column-ref","description":"Column name for the creating user reference","default":"created_by"},"updated_by_field":{"type":"string","format":"column-ref","description":"Column name for the last-updating user reference","default":"updated_by"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from created_by and updated_by to the users table","default":false}}}'::jsonb, + '{"type":"object","properties":{"created_by_field":{"type":"string","format":"column-ref","description":"Column name for the creating user reference","default":"created_by"},"updated_by_field":{"type":"string","format":"column-ref","description":"Column name for the last-updating user reference","default":"updated_by"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from created_by and updated_by to the users table","default":false},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the peoplestamp columns","default":true}}}'::jsonb, '{"timestamps","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -863,7 +863,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'data', 'Publishable', 'Adds publish state columns (is_published, published_at) for content visibility. Enables AuthzPublishable and AuthzTemporal authorization.', - '{"type":"object","properties":{"is_published_field":{"type":"string","format":"column-ref","description":"Column name for the published boolean flag","default":"is_published"},"published_at_field":{"type":"string","format":"column-ref","description":"Column name for the publish timestamp","default":"published_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, + '{"type":"object","properties":{"is_published_field_name":{"type":"string","format":"column-ref","description":"Column name for the published boolean flag","default":"is_published"},"published_at_field_name":{"type":"string","format":"column-ref","description":"Column name for the publish timestamp","default":"published_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, '{"publishing","temporal","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -873,6 +873,30 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema = EXCLUDED.parameter_schema, tags = EXCLUDED.tags; +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'DataRealtime', + 'data_realtime', + 'data', + 'Realtime Subscriptions', + 'Creates per-table subscriber tables in subscriptions_public with RLS policies derived from source table SELECT policies. Attaches statement-level triggers to emit changes to subscribers.', + '{"type":"object","properties":{"operations":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Which DML operations to track with emit_change triggers","default":["INSERT","UPDATE","DELETE"]},"subscriber_table_name":{"type":"string","description":"Custom name for the subscriber table (defaults to {source_table}_subscriber)"}}}'::jsonb, + '{"realtime","subscriptions","triggers"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + INSERT INTO metaschema_public.node_type_registry ( name, slug, @@ -887,7 +911,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'data', 'Slug', 'Auto-generates URL-friendly slugs from field values on insert/update. Attaches BEFORE INSERT and BEFORE UPDATE triggers that call inflection.slugify() on the target field. References fields by name in data jsonb.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to slugify"},"source_field_name":{"type":"string","format":"column-ref","description":"Optional source field name (defaults to field_name)"}},"required":["field_name"]}'::jsonb, + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to slugify","default":"slug"},"source_field_name":{"type":"string","format":"column-ref","description":"Optional source field name (defaults to field_name)"}},"required":[]}'::jsonb, '{"transform","behavior"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -959,7 +983,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'data', 'Tags', 'Adds a citext[] tags column with GIN index for efficient array containment queries (@>, &&). Standard tagging pattern for categorization and filtering.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the tags array","default":"tags"},"default_value":{"type":"string","description":"Default value expression for the tags column"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false}}}'::jsonb, + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the tags array","default":"tags"},"default_value":{"type":"string","description":"Default value expression for the tags column","default":"ARRAY[]::citext[]"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false}}}'::jsonb, '{"tags","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -993,6 +1017,270 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema = EXCLUDED.parameter_schema, tags = EXCLUDED.tags; +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'EventReferral', + 'event_referral', + 'event', + 'Event Referral', + 'Creates triggers that record events for the referrer (inviter) when their invitees perform actions on a watched table. Resolves the referrer automatically via the invites module''s claimed_invites table using the membership_type context. Supports the same compound condition system as EventTracker. Use with achievements to unlock levels and grant credits based on invitee activity.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record for the referrer (e.g., \"invitee_uploaded_avatar\", \"invitee_completed_onboarding\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the invitee (actor) ID on the source table — used to look up the referrer via claimed_invites.receiver_id","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped referral events. Omit for user-only events."},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}'::jsonb, + '{"events","referral","invites","analytics","tracking"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'EventTracker', + 'event_tracker', + 'event', + 'Event Tracker', + 'Creates triggers that record events via the events module when table rows change. Supports the same compound condition system as JobTrigger (condition_field, watch_fields, or full AND/OR/NOT conditions). Events are recorded to app_events and aggregated automatically. Use with achievements (blueprint-level) to unlock levels and grant credits based on event accumulation.', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record (e.g., \"avatar_uploaded\", \"order_completed\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"count":{"type":"integer","description":"Number of events to record per trigger fire","default":1},"toggle":{"type":"boolean","description":"Toggle mode: records event when condition is met, removes when condition is unmet","default":false},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the actor (user) ID to attribute the event to","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped events. Omit for user-only events."},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}'::jsonb, + '{"events","triggers","analytics","tracking"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'JobTrigger', + 'data_job_trigger', + 'job', + 'Job Trigger', + 'Dynamically creates PostgreSQL triggers that enqueue jobs via app_jobs.add_job() when table rows are inserted, updated, or deleted. Supports configurable payload strategies (full row, row ID, selected fields, or custom mapping), conditional firing via WHEN clauses, watched field changes, and extended job options (queue, priority, delay, max attempts).', + '{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"task_identifier":{"type":"string","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe)"},"payload_strategy":{"type":"string","enum":["row","row_id","fields","custom"],"description":"How to build the job payload: row (full NEW/OLD), row_id (just id), fields (selected columns), custom (mapped columns)","default":"row_id"},"payload_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names to include in payload (only for fields strategy)"},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Key-to-column mapping for custom payload (e.g., {\"invoice_id\": \"id\", \"total\": \"amount\"})"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Trigger events to create","default":["INSERT","UPDATE"]},"include_old":{"type":"boolean","description":"Include OLD row in payload (for UPDATE triggers)","default":false},"include_meta":{"type":"boolean","description":"Include table/schema metadata in payload","default":false},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"},"job_key":{"type":"string","description":"Static job key for upsert semantics (prevents duplicate jobs)"},"queue_name":{"type":"string","description":"Job queue name for routing to specific workers"},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"run_at_delay":{"type":"string","description":"Delay before job runs as PostgreSQL interval (e.g., 30 seconds, 5 minutes)"},"max_attempts":{"type":"integer","description":"Maximum retry attempts for the job","default":25}},"required":["task_identifier"]}'::jsonb, + '{"jobs","triggers","async"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitEnforceAggregate', + 'limit_enforce_aggregate', + 'limit_enforce', + 'Aggregate Limit Counter', + 'Declaratively attaches aggregate limit-tracking triggers to a table. On INSERT the named limit is incremented per entity; on DELETE it is decremented. Uses org_limit_aggregates_inc/dec for per-entity (org-level) aggregate limits rather than per-user limits. Requires a provisioned limits_module for the target database.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to track (must match a default_limits entry, e.g. \"databases\", \"members\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for aggregate limit lookup","default":"entity_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","aggregates","billing"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitEnforceCounter', + 'limit_enforce_counter', + 'limit_enforce', + 'Enforce Counter', + 'Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to track (must match a default_limits entry, e.g. \"projects\", \"members\")"},"scope":{"type":"string","enum":["app","org"],"description":"Limit scope: \"app\" (membership_type=1, user-level) or \"org\" (membership_type=2, entity-level)","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor or entity id used for limit lookup","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","enforce"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'LimitEnforceFeature', + 'limit_enforce_feature', + 'limit_enforce', + 'Feature Flag', + 'Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0).', + '{"type":"object","properties":{"feature_name":{"type":"string","description":"Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1)"},"scope":{"type":"string","enum":["app","org"],"description":"Feature scope: \"app\" (membership_type=1, app-level caps) or \"org\" (membership_type=2, per-entity caps)","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope)","default":"entity_id"}},"required":["feature_name"]}'::jsonb, + '{"limits","triggers","feature-flags","billing","caps"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessChunks', + 'data_chunks', + 'process', + 'Chunks', + 'Creates a chunked-embedding child table for any parent table. Provisions the chunks table with content, chunk_index, embedding vector, metadata, HNSW index, inherited RLS, and optional job trigger for automatic text splitting. Composed internally by ProcessFileEmbedding (enabled by default in extract mode) but can also be used standalone.', + '{"type":"object","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"dimensions":{"type":"integer","description":"Vector dimensions for per-chunk embeddings","default":768},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric for the HNSW index on chunk embeddings","default":"cosine"},"chunks_table_name":{"type":"string","description":"Override the chunks table name. Defaults to {parent_table}_chunks."},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from the parent table to copy into chunk metadata"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to create a job trigger that auto-enqueues chunking on parent INSERT/UPDATE","default":true} ,"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"},"embedding_model":{"type":"string","description":"Embedding model identifier for per-chunk embeddings. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."}}}'::jsonb, + '{"embedding","chunks","vector","ai","rag"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessExtraction', + 'process_extraction', + 'process', + 'File Extraction', + 'Creates extraction output fields and a job trigger for file text extraction. Fires when a file is uploaded (status = ''uploaded'') or on INSERT. The external worker extracts text/metadata from the file (PDF, DOCX, HTML, etc.) and writes the result back to the configured output fields. Typically used upstream of ProcessFileEmbedding or ProcessChunks.', + '{"type":"object","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''application/pdf'', ''text/%''], [''application/vnd.openxmlformats%''].","default":["application/pdf","text/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the extraction worker","default":"extract_file_text"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks (e.g., status = ''uploaded'').","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"queue_name":{"type":"string","description":"Job queue name for extraction tasks","default":"extraction"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5} ,"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"extraction_model":{"type":"string","description":"Extraction model identifier (e.g. a vision model for OCR, an LLM for structured extraction). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config."},"extraction_provider":{"type":"string","description":"Extraction provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."}}}'::jsonb, + '{"extraction","files","processing","jobs","text"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessFileEmbedding', + 'data_file_embedding', + 'process', + 'File Embedding', + 'Generic, MIME-scoped embedding node for file tables. Supports two modes: direct (whole-file to single vector, e.g. CLIP for images) when extraction is omitted, or extract (file to text to chunks to per-chunk vectors) when extraction config is provided. Composes SearchVector + JobTrigger + ProcessChunks (enabled by default in extract mode) internally. Multiple instances can coexist on the same table with different MIME scopes, field names, and embedding strategies.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 512 for CLIP, 768 for nomic, 1536 for ada-002)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''image/%''], [''application/pdf'', ''text/%''], [''audio/%''].","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker.","default":"process_file_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks, field guards, etc.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"extraction":{"type":"object","description":"Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"}}},"include_chunks":{"type":"boolean","description":"Whether to create a chunks table via ProcessChunks. Defaults to true when extraction is provided, false in direct mode. Set explicitly to override."},"chunks":{"type":"object","description":"Chunking configuration passed through to ProcessChunks. When include_chunks is true (or defaults to true in extract mode), these params configure the chunks table, embedding dimensions, strategy, etc.","default":{},"properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from parent to copy into chunk metadata"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true} ,"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \"nomic-embed-text\", \"text-embedding-3-small\", \"clip-vit-base-patch32\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."}}}'::jsonb, + '{"embedding","vector","ai","composition","jobs","multimodal","files"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessImageEmbedding', + 'data_image_embedding', + 'process', + 'Image Embedding', + 'Image-specific preset of ProcessFileEmbedding. Delegates to ProcessFileEmbedding with image-oriented defaults: dimensions=512 (CLIP), mime_patterns=[''image/%''], task_identifier=''process_image_embedding'', direct mode (no extraction). Accepts all ProcessFileEmbedding parameters — any overrides are forwarded through.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (default 512 for CLIP-style image embeddings)","default":512},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image embedding worker","default":"process_image_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"extraction":{"type":"object","description":"Text extraction configuration. Forwarded to ProcessFileEmbedding. When present, enables extract mode (e.g., OCR for images).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata","default":"extracted_metadata"}}},"chunks":{"type":"object","description":"Chunking configuration. Forwarded to ProcessFileEmbedding. Only meaningful when extraction is also provided.","properties":{"content_field_name":{"type":"string","format":"column-ref","default":"content"},"chunk_size":{"type":"integer","default":1000},"chunk_overlap":{"type":"integer","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"default":"paragraph"},"metadata_fields":{"type":"object"},"enqueue_chunking_job":{"type":"boolean","default":true} ,"chunking_task_name":{"type":"string","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \"clip-vit-base-patch32\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."}}}'::jsonb, + '{"embedding","image","vector","ai","composition","jobs"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'ProcessImageVersions', + 'process_image_versions', + 'process', + 'Image Versions', + 'Creates a job trigger for image variant generation. Fires when an image file is uploaded (status = ''uploaded'') or on INSERT. The external worker generates resized, cropped, or reformatted versions (thumbnails, previews, WebP conversions, etc.) and stores them as new file records linked to the source image.', + '{"type":"object","required":["versions"],"properties":{"versions":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Version identifier (e.g., \"thumb\", \"preview\", \"hero\")"},"width":{"type":"integer","description":"Target width in pixels"},"height":{"type":"integer","description":"Target height in pixels"},"fit":{"type":"string","enum":["cover","contain","fill","inside","outside"],"description":"Resize fitting strategy","default":"cover"},"format":{"type":"string","enum":["jpeg","png","webp","avif"],"description":"Output image format","default":"webp"},"quality":{"type":"integer","description":"Output quality (1-100)","default":80}},"required":["name"]},"description":"Array of version definitions. Each version specifies dimensions, format, and quality for a generated image variant. Required — the blueprint must explicitly define what variants to generate.","minItems":1},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Defaults to all image types.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image processing worker","default":"process_image_versions"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"queue_name":{"type":"string","description":"Job queue name for image processing tasks","default":"image_processing"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0}}}'::jsonb, + '{"images","processing","jobs","resize","thumbnails","files"}'::text[] +) ON CONFLICT (name) DO UPDATE SET + slug = EXCLUDED.slug, + category = EXCLUDED.category, + display_name = EXCLUDED.display_name, + description = EXCLUDED.description, + parameter_schema = EXCLUDED.parameter_schema, + tags = EXCLUDED.tags; + INSERT INTO metaschema_public.node_type_registry ( name, slug, @@ -1247,7 +1535,7 @@ INSERT INTO metaschema_public.node_type_registry ( 'search', 'Unified Search', 'Composite node type that orchestrates multiple search modalities (full-text search, BM25, embeddings, trigram) on a single table. Configures per-table search score weights, normalization strategy, and recency boost via the @searchConfig smart tag.', - '{"type":"object","properties":{"full_text_search":{"type":"object","description":"SearchFullText parameters. Omit to skip FTS setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref"},"weight":{"type":"string","enum":["A","B","C","D"]},"lang":{"type":"string"}},"required":["field"]}},"search_score_weight":{"type":"number","default":1}}},"bm25":{"type":"object","description":"SearchBm25 parameters. Omit to skip BM25 setup.","properties":{"field_name":{"type":"string","format":"column-ref"},"text_config":{"type":"string","default":"english"},"k1":{"type":"number"},"b":{"type":"number"},"search_score_weight":{"type":"number","default":1}}},"embedding":{"type":"object","description":"SearchVector parameters. Omit to skip embedding setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"embedding"},"dimensions":{"type":"integer","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"]},"metric":{"type":"string","enum":["cosine","l2","ip"]},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"}},"search_score_weight":{"type":"number","default":1},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}},"trgm_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to tag with @trgmSearch for fuzzy/typo-tolerant matching"},"search_config":{"type":"object","description":"Unified search score configuration written to @searchConfig smart tag","properties":{"weights":{"type":"object","description":"Per-algorithm weights: {tsv: 1.5, bm25: 1.0, pgvector: 0.8, trgm: 0.3}"},"normalization":{"type":"string","enum":["linear","sigmoid"],"description":"Score normalization strategy","default":"linear"},"boost_recent":{"type":"boolean","description":"Enable recency boost for search results","default":false},"boost_recency_field":{"type":"string","format":"column-ref","description":"Timestamp field for recency boost (e.g. created_at, updated_at)"},"boost_recency_decay":{"type":"number","description":"Decay rate for recency boost (0-1, lower = faster decay)","default":0.5}}}}}'::jsonb, + '{"type":"object","properties":{"full_text_search":{"type":"object","description":"SearchFullText parameters. Omit to skip FTS setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref"},"weight":{"type":"string","enum":["A","B","C","D"]},"lang":{"type":"string"}},"required":["field"]}},"search_score_weight":{"type":"number","default":1}}},"bm25":{"type":"object","description":"SearchBm25 parameters. Omit to skip BM25 setup.","properties":{"field_name":{"type":"string","format":"column-ref"},"text_config":{"type":"string","default":"english"},"k1":{"type":"number"},"b":{"type":"number"},"search_score_weight":{"type":"number","default":1}}},"embedding":{"type":"object","description":"SearchVector parameters. Omit to skip embedding setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"embedding"},"dimensions":{"type":"integer","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"]},"metric":{"type":"string","enum":["cosine","l2","ip"]},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"}},"search_score_weight":{"type":"number","default":1},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier. When null, the worker falls back to runtime config."},"embedding_provider":{"type":"string","description":"Embedding provider name. When null, the worker falls back to runtime config."}}},"embedding_text_field":{"type":"string","format":"column-ref","description":"Name of the composite text field created for embedding input","default":"embedding_text"},"composite_format":{"type":"string","enum":["labeled","plain"],"description":"Output format for the composite text field","default":"labeled"},"trgm_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to tag with @trgmSearch for fuzzy/typo-tolerant matching"},"search_config":{"type":"object","description":"Unified search score configuration written to @searchConfig smart tag","properties":{"weights":{"type":"object","description":"Per-algorithm weights: {tsv: 1.5, bm25: 1.0, pgvector: 0.8, trgm: 0.3}"},"normalization":{"type":"string","enum":["linear","sigmoid"],"description":"Score normalization strategy","default":"linear"},"boost_recent":{"type":"boolean","description":"Enable recency boost for search results","default":false},"boost_recency_field":{"type":"string","format":"column-ref","description":"Timestamp field for recency boost (e.g. created_at, updated_at)"},"boost_recency_decay":{"type":"number","description":"Decay rate for recency boost (0-1, lower = faster decay)","default":0.5}}}}}'::jsonb, '{"search","composite","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, @@ -1270,8 +1558,8 @@ INSERT INTO metaschema_public.node_type_registry ( 'search_vector', 'search', 'Vector Search', - 'Adds a vector embedding column with HNSW or IVFFlat index for similarity search. Supports configurable dimensions, distance metrics (cosine, l2, ip), stale tracking strategies (column, null, hash), and automatic job enqueue triggers for embedding generation.', - '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 384, 768, 1536, 3072)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric (cosine, l2, ip)","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"include_stale_field":{"type":"boolean","description":"When stale_strategy is column, adds an embedding_stale boolean field","default":true},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names that feed the embedding. Used by stale trigger to detect content changes."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","description":"Task identifier for the job queue","default":"generate_embedding"},"stale_strategy":{"type":"string","enum":["column","null","hash"],"description":"Strategy for tracking embedding staleness. column: embedding_stale boolean. null: set embedding to NULL. hash: md5 hash of source fields.","default":"column"},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}}'::jsonb, + 'Adds a vector embedding column with HNSW or IVFFlat index for similarity search. Supports configurable dimensions, distance metrics (cosine, l2, ip), per-field {field_name}_updated_at timestamp tracking (read-only in GraphQL), and automatic job enqueue triggers for embedding generation.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 384, 768, 1536, 3072)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric (cosine, l2, ip)","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names that feed the embedding. Used by stale trigger to detect content changes."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","description":"Task identifier for the job queue","default":"generate_embedding"},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \"nomic-embed-text\", \"text-embedding-3-small\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \"ollama\", \"openai\"). When null, the worker falls back to runtime config."}}}'::jsonb, '{"embedding","vector","ai","schema"}'::text[] ) ON CONFLICT (name) DO UPDATE SET slug = EXCLUDED.slug, 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 5beefa08d..a0c49e623 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,6 +33,11 @@ CREATE TABLE metaschema_public.table ( tags citext[] NOT NULL DEFAULT '{}', + partitioned boolean NOT NULL DEFAULT false, + partition_strategy text DEFAULT NULL, + partition_key_names text[] DEFAULT NULL, + partition_key_types text[] DEFAULT NULL, + 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/jest.config.js b/packages/metaschema-schema/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/metaschema-schema/jest.config.js +++ b/packages/metaschema-schema/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/metaschema-schema/package.json b/packages/metaschema-schema/package.json index 582e61f59..80bbb9b11 100644 --- a/packages/metaschema-schema/package.json +++ b/packages/metaschema-schema/package.json @@ -27,7 +27,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", 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 e70406b14..432dfdca5 100644 --- a/packages/metaschema-schema/sql/metaschema-schema--0.15.5.sql +++ b/packages/metaschema-schema/sql/metaschema-schema--0.15.5.sql @@ -70,7 +70,7 @@ ALTER TABLE metaschema_public.schema CREATE INDEX schema_database_id_idx ON metaschema_public.schema (database_id); -CREATE TABLE metaschema_public."table" ( +CREATE TABLE metaschema_public.table ( id uuid PRIMARY KEY DEFAULT uuidv7(), database_id uuid NOT NULL DEFAULT uuid_nil(), schema_id uuid NOT NULL, @@ -87,6 +87,10 @@ CREATE TABLE metaschema_public."table" ( plural_name text, singular_name text, tags citext[] NOT NULL DEFAULT '{}', + partitioned boolean NOT NULL DEFAULT false, + partition_strategy text DEFAULT NULL, + partition_key_names text[] DEFAULT NULL, + partition_key_types text[] DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -98,14 +102,14 @@ CREATE TABLE metaschema_public."table" ( UNIQUE (database_id, schema_id, name) ); -ALTER TABLE metaschema_public."table" +ALTER TABLE metaschema_public.table ADD COLUMN inherits_id uuid NULL - REFERENCES metaschema_public."table" (id); + REFERENCES metaschema_public.table (id); -CREATE INDEX table_schema_id_idx ON metaschema_public."table" (schema_id); +CREATE INDEX table_schema_id_idx ON metaschema_public.table (schema_id); -CREATE INDEX table_database_id_idx ON metaschema_public."table" (database_id); +CREATE INDEX table_database_id_idx ON metaschema_public.table (database_id); CREATE TABLE metaschema_public.check_constraint ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -126,7 +130,7 @@ CREATE TABLE metaschema_public.check_constraint ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (table_id, name), CHECK (field_ids <> '{}') @@ -136,7 +140,9 @@ CREATE INDEX check_constraint_table_id_idx ON metaschema_public.check_constraint CREATE INDEX check_constraint_database_id_idx ON metaschema_public.check_constraint (database_id); -CREATE FUNCTION metaschema_private.database_name_hash(name text) RETURNS bytea AS $EOFCODE$ +CREATE FUNCTION metaschema_private.database_name_hash( + name text +) RETURNS bytea AS $EOFCODE$ SELECT DECODE(MD5(LOWER(inflection.plural (name))), 'hex'); $EOFCODE$ LANGUAGE sql IMMUTABLE; @@ -172,7 +178,7 @@ CREATE TABLE metaschema_public.field ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (table_id, name) ); @@ -197,7 +203,7 @@ CREATE TABLE metaschema_public.foreign_key_constraint ( smart_tags jsonb, type text, field_ids uuid[] NOT NULL, - ref_table_id uuid NOT NULL REFERENCES metaschema_public."table" (id) + 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', @@ -212,7 +218,7 @@ CREATE TABLE metaschema_public.foreign_key_constraint ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (table_id, name), CHECK (field_ids <> '{}'), @@ -237,7 +243,7 @@ CREATE TABLE metaschema_public.full_text_search ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CHECK ( cardinality(field_ids) = cardinality(weights) @@ -273,7 +279,7 @@ CREATE TABLE metaschema_public.index ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (database_id, name) ); @@ -304,7 +310,7 @@ CREATE TABLE metaschema_public.policy ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (table_id, name) ); @@ -331,7 +337,7 @@ CREATE TABLE metaschema_public.primary_key_constraint ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (table_id, name), CHECK (field_ids <> '{}') @@ -374,7 +380,7 @@ CREATE TABLE metaschema_public.table_grant ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); @@ -382,12 +388,14 @@ CREATE INDEX table_grant_table_id_idx ON metaschema_public.table_grant (table_id CREATE INDEX table_grant_database_id_idx ON metaschema_public.table_grant (database_id); -CREATE FUNCTION metaschema_private.table_name_hash(name text) RETURNS bytea AS $EOFCODE$ +CREATE FUNCTION metaschema_private.table_name_hash( + name text +) RETURNS bytea AS $EOFCODE$ SELECT DECODE(MD5(LOWER(inflection.plural (name))), 'hex'); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE UNIQUE INDEX databases_table_unique_name_idx ON metaschema_public."table" (database_id, schema_id, (metaschema_private.table_name_hash(name))); +CREATE UNIQUE INDEX databases_table_unique_name_idx ON metaschema_public.table (database_id, schema_id, (metaschema_private.table_name_hash(name))); CREATE TABLE metaschema_public.trigger_function ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -421,7 +429,7 @@ CREATE TABLE metaschema_public.trigger ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (table_id, name) ); @@ -449,7 +457,7 @@ CREATE TABLE metaschema_public.unique_constraint ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (table_id, name), CHECK (field_ids <> '{}') @@ -486,7 +494,7 @@ CREATE TABLE metaschema_public.view ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (schema_id, name) ); @@ -508,7 +516,7 @@ CREATE TABLE metaschema_public.view_table ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, UNIQUE (view_id, table_id) ); @@ -600,7 +608,7 @@ CREATE TABLE metaschema_public.enum ( name text NOT NULL, label text, description text, - values text[] NOT NULL DEFAULT '{}', + "values" text[] NOT NULL DEFAULT '{}', smart_tags jsonb, category metaschema_public.object_category NOT NULL DEFAULT 'app', module text NULL, @@ -635,8 +643,11 @@ CREATE TABLE metaschema_public.embedding_chunks ( chunk_overlap int NOT NULL DEFAULT 200, chunk_strategy text NOT NULL DEFAULT 'fixed', metadata_fields jsonb, + search_indexes jsonb, enqueue_chunking_job boolean NOT NULL DEFAULT true, chunking_task_name text NOT NULL DEFAULT 'generate_chunks', + embedding_model text, + embedding_provider text, parent_fk_field_id uuid, CONSTRAINT db_fkey FOREIGN KEY(database_id) @@ -644,11 +655,11 @@ CREATE TABLE metaschema_public.embedding_chunks ( ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY(table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT chunks_table_fkey FOREIGN KEY(chunks_table_id) - REFERENCES metaschema_public."table" (id) + REFERENCES metaschema_public.table (id) ON DELETE SET NULL, CONSTRAINT embedding_field_fkey FOREIGN KEY(embedding_field_id) @@ -699,7 +710,7 @@ CREATE TABLE metaschema_public.spatial_relation ( 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) @@ -707,7 +718,7 @@ CREATE TABLE metaschema_public.spatial_relation ( 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 ref_field_fkey FOREIGN KEY(ref_field_id) @@ -754,7 +765,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzAllowAll', 'authz_allow_all', 'authz', 'Public Access', 'Allows all access. Generates TRUE expression.', '{"type":"object","properties":{}}'::jsonb, CAST('{"authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzAllowAll', 'authz_allow_all', 'authz', 'Public Access', 'Allows all access. Generates TRUE expression.', '{"type":"object","properties":{}}'::jsonb, CAST('{"authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -765,7 +782,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzAppMembership', 'authz_app_membership_check', 'authz', 'App Membership Check', 'App-level membership check (hardcoded membership_type=1). Verifies the user has app membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. For entity-scoped checks (org, channel, etc.), use AuthzEntityMembership instead.', CAST('{"type":"object","properties":{"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":[]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzAppMembership', 'authz_app_membership_check', 'authz', 'App Membership Check', 'App-level membership check (hardcoded membership_type=1). Verifies the user has app membership (optionally with specific permission) without binding to any entity from the row. Uses EXISTS subquery against SPRT table. For entity-scoped checks (org, channel, etc.), use AuthzEntityMembership instead.', CAST('{"type":"object","properties":{"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":[]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -776,7 +799,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzComposite', 'authz_composite', 'authz', 'Composite Policy', 'Composite authorization policy that combines multiple authorization nodes using boolean logic (AND/OR). The data field contains a JSONB AST with nested authorization nodes.', '{"type":"object","description":"A composite policy containing nested authorization nodes combined with boolean logic","properties":{"BoolExpr":{"type":"object","description":"Boolean expression combining multiple authorization nodes","properties":{"boolop":{"type":"string","enum":["AND_EXPR","OR_EXPR","NOT_EXPR"],"description":"Boolean operator: AND_EXPR, OR_EXPR, or NOT_EXPR"},"args":{"type":"array","description":"Array of authorization nodes to combine","items":{"type":"object"}}}}}}'::jsonb, CAST('{"composite","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzComposite', 'authz_composite', 'authz', 'Composite Policy', 'Composite authorization policy that combines multiple authorization nodes using boolean logic (AND/OR). The data field contains a JSONB AST with nested authorization nodes.', '{"type":"object","description":"A composite policy containing nested authorization nodes combined with boolean logic","properties":{"BoolExpr":{"type":"object","description":"Boolean expression combining multiple authorization nodes","properties":{"boolop":{"type":"string","enum":["AND_EXPR","OR_EXPR","NOT_EXPR"],"description":"Boolean operator: AND_EXPR, OR_EXPR, or NOT_EXPR"},"args":{"type":"array","description":"Array of authorization nodes to combine","items":{"type":"object"}}}}}}'::jsonb, CAST('{"composite","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -787,7 +816,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzDenyAll', 'authz_deny_all', 'authz', 'No Access', 'Denies all access. Generates FALSE expression.', '{"type":"object","properties":{}}'::jsonb, CAST('{"authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzDenyAll', 'authz_deny_all', 'authz', 'No Access', 'Denies all access. Generates FALSE expression.', '{"type":"object","properties":{}}'::jsonb, CAST('{"authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -798,7 +833,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzDirectOwner', 'authz_direct_owner', 'authz', 'Direct Ownership', 'Direct equality comparison between a table column and the current user ID. Simplest authorization pattern with no subqueries.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)"}},"required":["entity_field"]}' AS jsonb), CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzDirectOwner', 'authz_direct_owner', 'authz', 'Direct Ownership', 'Direct equality comparison between a table column and the current user ID. Simplest authorization pattern with no subqueries.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)"}},"required":["entity_field"]}' AS jsonb), CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -809,7 +850,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzDirectOwnerAny', 'authz_direct_owner_any', 'authz', 'Multi-Owner Access', 'OR logic for multiple ownership fields. Checks if current user matches any of the specified fields.', '{"type":"object","properties":{"entity_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of column names to check for ownership"}},"required":["entity_fields"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzDirectOwnerAny', 'authz_direct_owner_any', 'authz', 'Multi-Owner Access', 'OR logic for multiple ownership fields. Checks if current user matches any of the specified fields.', '{"type":"object","properties":{"entity_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of column names to check for ownership"}},"required":["entity_fields"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -820,7 +867,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzEntityMembership', 'authz_entity_membership', 'authz', 'Entity Membership', 'Membership check scoped by a field on the row through the SPRT table. Verifies user has membership in the entity referenced by the row.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzEntityMembership', 'authz_entity_membership', 'authz', 'Entity Membership', 'Membership check scoped by a field on the row through the SPRT table. Verifies user has membership in the entity referenced by the row.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -831,7 +884,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzFilePath', 'authz_file_path', 'authz', 'File Path Share', 'Path-scoped file sharing via ltree containment. Grants access when a path_shares row matches the current user, bucket, and an ancestor path with the required permission.', CAST('{"type":"object","properties":{"shares_schema":{"type":"string","description":"Schema of the path_shares table"},"shares_table":{"type":"string","description":"Name of the path_shares table"},"files_schema":{"type":"string","description":"Schema of the files table (used to qualify column references inside the EXISTS subquery)"},"files_table":{"type":"string","description":"Name of the files table (used to qualify column references inside the EXISTS subquery)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["shares_schema","shares_table","files_table","permission_field"]}' AS jsonb), CAST('{"storage","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzFilePath', 'authz_file_path', 'authz', 'File Path Share', 'Path-scoped file sharing via ltree containment. Grants access when a path_shares row matches the current user, bucket, and an ancestor path with the required permission.', CAST('{"type":"object","properties":{"shares_schema":{"type":"string","description":"Schema of the path_shares table"},"shares_table":{"type":"string","description":"Name of the path_shares table"},"files_schema":{"type":"string","description":"Schema of the files table (used to qualify column references inside the EXISTS subquery)"},"files_table":{"type":"string","description":"Name of the files table (used to qualify column references inside the EXISTS subquery)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["shares_schema","shares_table","files_table","permission_field"]}' AS jsonb), CAST('{"storage","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -842,7 +901,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzMemberList', 'authz_member_list', 'authz', 'Member List', 'Check if current user is in an array column on the same row.', '{"type":"object","properties":{"array_field":{"type":"string","format":"column-ref","description":"Column name containing the array of user IDs"}},"required":["array_field"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzMemberList', 'authz_member_list', 'authz', 'Member List', 'Check if current user is in an array column on the same row.', '{"type":"object","properties":{"array_field":{"type":"string","format":"column-ref","description":"Column name containing the array of user IDs"}},"required":["array_field"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -853,7 +918,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzNotReadOnly', 'authz_not_read_only', 'authz', 'Not Read-Only', 'Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 2=org, 3+=dynamic entity types. Must be >= 2 (entity-scoped)."}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz","restrictive"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzNotReadOnly', 'authz_not_read_only', 'authz', 'Not Read-Only', 'Restrictive policy that blocks read-only members from mutations. Checks actor_id + is_read_only IS NOT TRUE on the SPRT. Designed to run as a restrictive counterpart after a permissive AuthzEntityMembership policy has already verified membership.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id, org_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 2=org, 3+=dynamic entity types. Must be >= 2 (entity-scoped)."}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz","restrictive"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -864,7 +935,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzOrgHierarchy', 'authz_org_hierarchy', 'authz', 'Org Hierarchy', 'Organizational hierarchy visibility using closure table. Managers can see subordinate data or subordinates can see manager data.', CAST('{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"],"description":"down=manager sees subordinates, up=subordinate sees managers"},"entity_field":{"type":"string","format":"column-ref","description":"Field referencing the org entity","default":"entity_id"},"anchor_field":{"type":"string","format":"column-ref","description":"Field referencing the user (e.g., owner_id)"},"max_depth":{"type":"integer","description":"Optional max depth to limit visibility"}},"required":["direction","anchor_field"]}' AS jsonb), CAST('{"membership","hierarchy","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzOrgHierarchy', 'authz_org_hierarchy', 'authz', 'Org Hierarchy', 'Organizational hierarchy visibility using closure table. Managers can see subordinate data or subordinates can see manager data.', CAST('{"type":"object","properties":{"direction":{"type":"string","enum":["up","down"],"description":"down=manager sees subordinates, up=subordinate sees managers"},"entity_field":{"type":"string","format":"column-ref","description":"Field referencing the org entity","default":"entity_id"},"anchor_field":{"type":"string","format":"column-ref","description":"Field referencing the user (e.g., owner_id)"},"max_depth":{"type":"integer","description":"Optional max depth to limit visibility"}},"required":["direction","anchor_field"]}' AS jsonb), CAST('{"membership","hierarchy","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -875,7 +952,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzPeerOwnership', 'authz_peer_ownership', 'authz', 'Peer Ownership', 'Peer visibility through shared entity membership. Authorizes access to user-owned rows when the owner and current user are both members of the same entity. Self-joins the SPRT table to find peers.', CAST('{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the owning user (e.g., owner_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["owner_field"]}' AS jsonb), CAST('{"membership","peer","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzPeerOwnership', 'authz_peer_ownership', 'authz', 'Peer Ownership', 'Peer visibility through shared entity membership. Authorizes access to user-owned rows when the owner and current user are both members of the same entity. Self-joins the SPRT table to find peers.', CAST('{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the owning user (e.g., owner_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["owner_field"]}' AS jsonb), CAST('{"membership","peer","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -886,7 +969,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzPublishable', 'authz_publishable', 'authz', 'Published Content', 'Published state access control. Restricts access to records that are published.', CAST('{"type":"object","properties":{"is_published_field":{"type":"string","format":"column-ref","description":"Boolean field indicating published state","default":"is_published"},"published_at_field":{"type":"string","format":"column-ref","description":"Timestamp field for publish time","default":"published_at"},"require_published_at":{"type":"boolean","description":"Require published_at to be non-null and <= now()","default":true}}}' AS jsonb), CAST('{"temporal","publishing","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzPublishable', 'authz_publishable', 'authz', 'Published Content', 'Published state access control. Restricts access to records that are published.', CAST('{"type":"object","properties":{"is_published_field":{"type":"string","format":"column-ref","description":"Boolean field indicating published state","default":"is_published"},"published_at_field":{"type":"string","format":"column-ref","description":"Timestamp field for publish time","default":"published_at"},"require_published_at":{"type":"boolean","description":"Require published_at to be non-null and <= now()","default":true}}}' AS jsonb), CAST('{"temporal","publishing","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -897,7 +986,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzRelatedEntityMembership', 'authz_related_entity_membership', 'authz', 'Related Entity Membership', 'JOIN-based membership verification through related tables. Joins SPRT table with another table to verify membership.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the join table"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"sprt_join_field":{"type":"string","description":"SPRT column to join on with the related table","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the join table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the join table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the join table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on join table (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on join table to match against SPRT entity_id"},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzRelatedEntityMembership', 'authz_related_entity_membership', 'authz', 'Related Entity Membership', 'JOIN-based membership verification through related tables. Joins SPRT table with another table to verify membership.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the join table"},"sel_field":{"type":"string","description":"SPRT column to select for the entity match","default":"entity_id"},"sprt_join_field":{"type":"string","description":"SPRT column to join on with the related table","default":"entity_id"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the join table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the join table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the join table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on join table (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on join table to match against SPRT entity_id"},"permission":{"type":"string","description":"Single permission name to check (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -908,7 +1003,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzRelatedMemberList', 'authz_related_member_list', 'authz', 'Related Member List', 'Array membership check in a related table.', '{"type":"object","properties":{"owned_schema":{"type":"string","description":"Schema of the related table"},"owned_table":{"type":"string","description":"Name of the related table"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_schema","owned_table","owned_table_key","owned_table_ref_key","this_object_key"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzRelatedMemberList', 'authz_related_member_list', 'authz', 'Related Member List', 'Array membership check in a related table.', '{"type":"object","properties":{"owned_schema":{"type":"string","description":"Schema of the related table"},"owned_table":{"type":"string","description":"Name of the related table"},"owned_table_key":{"type":"string","format":"column-ref","description":"Array column in related table"},"owned_table_ref_key":{"type":"string","format":"column-ref","description":"FK column in related table"},"this_object_key":{"type":"string","format":"column-ref","description":"PK column in protected table"}},"required":["owned_schema","owned_table","owned_table_key","owned_table_ref_key","this_object_key"]}'::jsonb, CAST('{"ownership","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -919,7 +1020,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzRelatedPeerOwnership', 'authz_related_peer_ownership', 'authz', 'Related Peer Ownership', 'Peer visibility through shared entity membership via a related table. Like AuthzPeerOwnership but the owning user is resolved through a FK JOIN to a related table. Combines SPRT self-join with object table JOIN.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the related table (e.g., message_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the related table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the related table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the related table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on related table containing the owner user ID (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on related table containing the owner user ID (e.g., sender_id)"},"obj_ref_field":{"type":"string","format":"column-ref","description":"Field on related table to select for matching entity_field","default":"id"},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","peer","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzRelatedPeerOwnership', 'authz_related_peer_ownership', 'authz', 'Related Peer Ownership', 'Peer visibility through shared entity membership via a related table. Like AuthzPeerOwnership but the owning user is resolved through a FK JOIN to a related table. Combines SPRT self-join with object table JOIN.', CAST('{"type":"object","properties":{"entity_field":{"type":"string","format":"column-ref","description":"Column name on protected table referencing the related table (e.g., message_id)"},"membership_type":{"type":["integer","string"],"description":"Scope: 1=app, 2=org, 3+=dynamic entity types (or string name resolved via membership_types_module)"},"entity_type":{"type":"string","description":"Entity type prefix (e.g. ''channel'', ''department''). Resolved to membership_type integer via memberships_module lookup. Use instead of membership_type for readability."},"obj_table_id":{"type":"string","format":"uuid","description":"UUID of the related table (alternative to obj_schema/obj_table)"},"obj_schema":{"type":"string","description":"Schema of the related table (or use obj_table_id)"},"obj_table":{"type":"string","description":"Name of the related table (or use obj_table_id)"},"obj_field_id":{"type":"string","format":"uuid","description":"UUID of field on related table containing the owner user ID (alternative to obj_field)"},"obj_field":{"type":"string","format":"column-ref","description":"Field name on related table containing the owner user ID (e.g., sender_id)"},"obj_ref_field":{"type":"string","format":"column-ref","description":"Field on related table to select for matching entity_field","default":"id"},"permission":{"type":"string","description":"Single permission name to check on the current user membership (resolved to bitstring mask)"},"permissions":{"type":"array","items":{"type":"string"},"description":"Multiple permission names to check on the current user membership (ORed together into mask)"},"is_admin":{"type":"boolean","description":"If true, require is_admin flag on current user membership"},"is_owner":{"type":"boolean","description":"If true, require is_owner flag on current user membership"}},"required":["entity_field"]}' AS jsonb), CAST('{"membership","peer","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -930,7 +1037,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('AuthzTemporal', 'authz_temporal', 'authz', 'Temporal Access', 'Time-window based access control. Restricts access based on valid_from and/or valid_until timestamps. At least one of valid_from_field or valid_until_field must be provided.', CAST('{"type":"object","properties":{"valid_from_field":{"type":"string","format":"column-ref","description":"Column for start time (at least one of valid_from_field or valid_until_field required)"},"valid_until_field":{"type":"string","format":"column-ref","description":"Column for end time (at least one of valid_from_field or valid_until_field required)"},"valid_from_inclusive":{"type":"boolean","description":"Include start boundary","default":true},"valid_until_inclusive":{"type":"boolean","description":"Include end boundary","default":false}},"anyOf":[{"required":["valid_from_field"]},{"required":["valid_until_field"]}]}' AS jsonb), CAST('{"temporal","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('AuthzTemporal', 'authz_temporal', 'authz', 'Temporal Access', 'Time-window based access control. Restricts access based on valid_from and/or valid_until timestamps. At least one of valid_from_field or valid_until_field must be provided.', CAST('{"type":"object","properties":{"valid_from_field":{"type":"string","format":"column-ref","description":"Column for start time (at least one of valid_from_field or valid_until_field required)"},"valid_until_field":{"type":"string","format":"column-ref","description":"Column for end time (at least one of valid_from_field or valid_until_field required)"},"valid_from_inclusive":{"type":"boolean","description":"Include start boundary","default":true},"valid_until_inclusive":{"type":"boolean","description":"Include end boundary","default":false}},"anyOf":[{"required":["valid_from_field"]},{"required":["valid_until_field"]}]}' AS jsonb), CAST('{"temporal","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -941,7 +1054,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataCompositeField', 'data_composite_field', 'data', 'Composite Field', 'Creates a derived text field that automatically concatenates multiple source fields via BEFORE INSERT/UPDATE triggers. Used to produce a unified text representation (e.g., embedding_text) from multiple columns on a table. The trigger fires with ''_000'' prefix to run before Search* triggers alphabetically.', CAST('{"type":"object","properties":{"target":{"type":"string","format":"column-ref","description":"Name of the derived text field to create (default: ''embedding_text'')"},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of source field names to concatenate into the target field"},"format":{"type":"string","enum":["labeled","plain"],"description":"Output format: ''labeled'' (field_name: value) or ''plain'' (values only). Default: ''labeled''"}},"required":["source_fields"]}' AS jsonb), CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('LimitTrackUsage', 'limit_track_usage', 'limit_track', 'Track Usage', 'Declaratively attaches billing usage-recording triggers to a table. On INSERT the named meter is incremented via record_usage; on DELETE it is decremented (reversal). On UPDATE, if the entity_field changes, the old entity is decremented and the new entity is incremented. Requires a provisioned billing_module for the target database.', CAST(E'{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to record usage against (must match a meters table entry, e.g. \\"databases\\", \\"seats\\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for billing","default":"entity_id"},"quantity":{"type":"integer","description":"Units to record per event (default 1)","default":1},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["meter_slug"]}' AS jsonb), CAST('{"billing","triggers","metering","usage","track"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -952,7 +1071,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataDirectOwner', 'data_direct_owner', 'data', 'Ownership', 'Adds ownership column for direct user ownership. Enables AuthzDirectOwner authorization.', '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for owner ID","default":"owner_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from owner_id to the users table","default":true}}}'::jsonb, CAST('{"ownership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('CheckGreaterThan', 'check_greater_than', 'check', 'Check Greater Than', 'Adds a CHECK constraint that validates a column value is greater than a threshold (single-column: column > value) or that one column is greater than another (cross-column: columns[0] > columns[1]). Compiled via AST helpers.', CAST('{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column > value)","default":0},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] > columns[1])","minItems":2,"maxItems":2}}}' AS jsonb), CAST('{"check","constraint","validation","comparison"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -963,7 +1088,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataEntityMembership', 'data_entity_membership', 'data', 'Entity Membership', 'Adds entity reference for organization/group scoping. Enables AuthzEntityMembership, AuthzMembership, AuthzOrgHierarchy authorization.', '{"type":"object","properties":{"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for entity ID","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from entity_id to the users table","default":true}}}'::jsonb, CAST('{"membership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('CheckLessThan', 'check_less_than', 'check', 'Check Less Than', 'Adds a CHECK constraint that validates a column value is less than a threshold (single-column: column < value) or that one column is less than another (cross-column: columns[0] < columns[1]). Compiled via AST helpers.', CAST('{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Single column to compare against value (mutually exclusive with columns)"},"value":{"type":"number","description":"Threshold value for single-column comparison (column < value)"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns for cross-column comparison (columns[0] < columns[1])","minItems":2,"maxItems":2}}}' AS jsonb), CAST('{"check","constraint","validation","comparison"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -974,7 +1105,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataFeatureFlag', 'data_feature_flag', 'data', 'Feature Flag', 'Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0).', CAST(E'{"type":"object","properties":{"feature_name":{"type":"string","description":"Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1)"},"scope":{"type":"string","enum":["app","org"],"description":"Feature scope: \\"app\\" (membership_type=1, app-level caps) or \\"org\\" (membership_type=2, per-entity caps)","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope)","default":"entity_id"}},"required":["feature_name"]}' AS jsonb), CAST('{"limits","triggers","feature-flags","billing","caps"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('CheckNotEqual', 'check_not_equal', 'check', 'Check Not Equal', 'Adds a CHECK constraint that validates two columns are not equal (columns[0] != columns[1]). Useful for preventing self-referencing rows. Compiled via AST helpers.', '{"type":"object","properties":{"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Two columns that must not be equal","minItems":2,"maxItems":2}},"required":["columns"]}'::jsonb, CAST('{"check","constraint","validation","inequality"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -985,7 +1122,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataFileEmbedding', 'data_file_embedding', 'data', 'File Embedding', 'Generic, MIME-scoped embedding node for file tables. Supports two modes: direct (whole-file to single vector, e.g. CLIP for images) when extraction is omitted, or extract (file to text to chunks to per-chunk vectors) when extraction config is provided. Composes SearchVector + DataJobTrigger internally. Multiple instances can coexist on the same table with different MIME scopes, field names, and embedding strategies.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 512 for CLIP, 768 for nomic, 1536 for ada-002)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''image/%''], [''application/pdf'', ''text/%''], [''audio/%''].","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker.","default":"process_file_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks, field guards, etc.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"extraction":{"type":"object","description":"Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"},"status_field":{"type":"string","format":"column-ref","description":"Extraction lifecycle status field","default":"extraction_status"}}},"chunks":{"type":"object","description":"Chunking configuration. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Only meaningful when extraction is also provided.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"stale_strategy":{"type":"string","enum":["column","null","hash"],"description":"Strategy for tracking embedding staleness when extraction is enabled. column: embedding_stale boolean. null: set embedding to NULL. hash: md5 hash.","default":"column"},"include_stale_field":{"type":"boolean","description":"Whether to include the embedding_stale boolean field (extract mode)","default":true}}}' AS jsonb), CAST('{"embedding","vector","ai","composition","jobs","multimodal","files"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('CheckOneOf', 'check_one_of', 'check', 'Check One Of', 'Adds a CHECK constraint that validates a column value is one of an allowed set (e.g. tier IN (''free'', ''paid'', ''custom'')). Compiled to column = ANY(ARRAY[...]) via AST helpers.', '{"type":"object","properties":{"column":{"type":"string","format":"column-ref","description":"Column to validate against the allowed values"},"values":{"type":"array","items":{"type":"string"},"description":"Array of allowed values for the column"}},"required":["column","values"]}'::jsonb, CAST('{"check","constraint","validation","enum"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -996,7 +1139,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataForceCurrentUser', 'data_force_current_user', 'data', 'Force Current User', 'BEFORE INSERT trigger that forces a field to the value of jwt_public.current_user_id(). Prevents clients from spoofing the actor/uploader identity. The field value is always overwritten regardless of what the client provides.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to force to current_user_id()","default":"actor_id"}}}' AS jsonb), CAST('{"trigger","security","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataBulk', 'data_bulk', 'data', 'Bulk Operations', 'Enables bulk mutation smart tags on a table. When provisioned, adds @behavior tags for the selected bulk operations (insert, upsert, update, delete). Requires the graphile-bulk-mutations plugin.', CAST('{"type":"object","properties":{"insert":{"type":"boolean","description":"Enable bulk insert (+bulkInsert)","default":true},"upsert":{"type":"boolean","description":"Enable bulk upsert (+bulkUpsert)","default":false},"update":{"type":"boolean","description":"Enable bulk update (+bulkUpdate)","default":false},"delete":{"type":"boolean","description":"Enable bulk delete (+bulkDelete)","default":false}}}' AS jsonb), CAST('{"bulk","mutations","graphile"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1007,7 +1156,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataId', 'data_id', 'data', 'Primary Key ID', 'Adds a UUID primary key column with auto-generation default (uuidv7). This is the standard primary key pattern for all tables.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the primary key","default":"id"}}}'::jsonb, CAST('{"primary_key","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataCompositeField', 'data_composite_field', 'data', 'Composite Field', 'Creates a derived text field that automatically concatenates multiple source fields via BEFORE INSERT/UPDATE triggers. Used to produce a unified text representation (e.g., embedding_text) from multiple columns on a table. The trigger fires with ''_000'' prefix to run before Search* triggers alphabetically.', CAST('{"type":"object","properties":{"target":{"type":"string","format":"column-ref","description":"Name of the derived text field to create","default":"embedding_text"},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Array of source field names to concatenate into the target field"},"format":{"type":"string","enum":["labeled","plain"],"description":"Output format: ''labeled'' (field_name: value) or ''plain'' (values only)","default":"labeled"}},"required":["source_fields"]}' AS jsonb), CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1018,7 +1173,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataImageEmbedding', 'data_image_embedding', 'data', 'Image Embedding', 'Composition wrapper that creates a vector embedding field with HNSW/IVFFlat index (via SearchVector) and a job trigger with compound conditions (via DataJobTrigger) that fires on INSERT for image files matching mime_type patterns. Designed for storage file tables.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions","default":512},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"task_identifier":{"type":"string","description":"Job task identifier for the embedding worker","default":"process_image_embedding"},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match (e.g., image/%, video/%). Multiple patterns are OR''d together.","default":["image/%"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}}}}' AS jsonb), CAST('{"embedding","image","vector","ai","composition","jobs"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataDirectOwner', 'data_direct_owner', 'data', 'Ownership', 'Adds ownership column for direct user ownership. Enables AuthzDirectOwner authorization.', '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for owner ID","default":"owner_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from owner_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the owner column","default":true}}}'::jsonb, CAST('{"ownership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1029,7 +1190,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataImmutableFields', 'data_immutable_fields', 'data', 'Immutable Fields', 'BEFORE UPDATE trigger that prevents changes to a list of specified fields after INSERT. Raises an exception if any of the listed fields have changed. Unlike FieldImmutable (single-field), this handles multiple fields in a single trigger for efficiency.', CAST(E'{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names that cannot be modified after INSERT (e.g. [\\"key\\", \\"bucket_id\\", \\"owner_id\\"])"}},"required":["fields"]}' AS jsonb), CAST('{"trigger","constraint","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataEntityMembership', 'data_entity_membership', 'data', 'Entity Membership', 'Adds entity reference for organization/group scoping. Enables AuthzEntityMembership, AuthzMembership, AuthzOrgHierarchy authorization.', '{"type":"object","properties":{"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for entity ID","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds a foreign key constraint from entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates a B-tree index on the entity column","default":true}}}'::jsonb, CAST('{"membership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1040,7 +1207,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataInflection', 'data_inflection', 'data', 'Inflection', 'Transforms field values using inflection operations (snake_case, camelCase, slugify, plural, singular, etc). Attaches BEFORE INSERT and BEFORE UPDATE triggers. References fields by name in data jsonb.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to transform"},"ops":{"type":"array","items":{"type":"string","enum":["plural","singular","camel","pascal","dashed","slugify","underscore","lower","upper"]},"description":"Inflection operations to apply in order"}},"required":["field_name","ops"]}'::jsonb, CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataForceCurrentUser', 'data_force_current_user', 'data', 'Force Current User', 'BEFORE INSERT trigger that forces a field to the value of jwt_public.current_user_id(). Prevents clients from spoofing the actor/uploader identity. The field value is always overwritten regardless of what the client provides.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to force to current_user_id()","default":"actor_id"}}}' AS jsonb), CAST('{"trigger","security","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1051,7 +1224,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataInheritFromParent', 'data_inherit_from_parent', 'data', 'Inherit From Parent', 'BEFORE INSERT trigger that copies specified fields from a parent table via a foreign key. The parent row is looked up through RLS (SECURITY INVOKER), so the insert fails if the caller cannot see the parent. Used by the storage module to inherit owner_id and is_public from buckets to files.', CAST(E'{"type":"object","properties":{"parent_fk_field":{"type":"string","format":"column-ref","description":"Name of the FK field on this table that references the parent (e.g. bucket_id)"},"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to copy from the parent row (e.g. [\\"owner_id\\", \\"is_public\\"])"},"parent_table":{"type":"string","description":"Parent table name (optional fallback if FK not yet registered in metaschema)"},"parent_schema":{"type":"string","description":"Parent table schema (optional, defaults to same schema as child table)"}},"required":["parent_fk_field","fields"]}' AS jsonb), CAST('{"trigger","inheritance","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataId', 'data_id', 'data', 'Primary Key ID', 'Adds a UUID primary key column with auto-generation default (uuidv7). This is the standard primary key pattern for all tables.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the primary key","default":"id"}}}'::jsonb, CAST('{"primary_key","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1062,7 +1241,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataJobTrigger', 'data_job_trigger', 'data', 'Job Trigger', 'Dynamically creates PostgreSQL triggers that enqueue jobs via app_jobs.add_job() when table rows are inserted, updated, or deleted. Supports configurable payload strategies (full row, row ID, selected fields, or custom mapping), conditional firing via WHEN clauses, watched field changes, and extended job options (queue, priority, delay, max attempts).', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"task_identifier":{"type":"string","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe)"},"payload_strategy":{"type":"string","enum":["row","row_id","fields","custom"],"description":"How to build the job payload: row (full NEW/OLD), row_id (just id), fields (selected columns), custom (mapped columns)","default":"row_id"},"payload_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names to include in payload (only for fields strategy)"},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Key-to-column mapping for custom payload (e.g., {\\"invoice_id\\": \\"id\\", \\"total\\": \\"amount\\"})"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Trigger events to create","default":["INSERT","UPDATE"]},"include_old":{"type":"boolean","description":"Include OLD row in payload (for UPDATE triggers)","default":false},"include_meta":{"type":"boolean","description":"Include table/schema metadata in payload","default":false},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"},"job_key":{"type":"string","description":"Static job key for upsert semantics (prevents duplicate jobs)"},"queue_name":{"type":"string","description":"Job queue name for routing to specific workers"},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"run_at_delay":{"type":"string","description":"Delay before job runs as PostgreSQL interval (e.g., 30 seconds, 5 minutes)"},"max_attempts":{"type":"integer","description":"Maximum retry attempts for the job","default":25}},"required":["task_identifier"]}' AS jsonb), CAST('{"jobs","triggers","async"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataImmutableFields', 'data_immutable_fields', 'data', 'Immutable Fields', 'BEFORE UPDATE trigger that prevents changes to a list of specified fields after INSERT. Raises an exception if any of the listed fields have changed. Unlike FieldImmutable (single-field), this handles multiple fields in a single trigger for efficiency.', CAST(E'{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names that cannot be modified after INSERT (e.g. [\\"key\\", \\"bucket_id\\", \\"owner_id\\"])"}},"required":["fields"]}' AS jsonb), CAST('{"trigger","constraint","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1073,7 +1258,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataJsonb', 'data_jsonb', 'data', 'JSONB Field', 'Adds a JSONB column with optional GIN index for containment queries (@>, ?, ?|, ?&). Standard pattern for semi-structured metadata.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the JSONB field","default":"metadata"},"default_value":{"type":"string","description":"Default value expression"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false},"create_index":{"type":"boolean","description":"Whether to create a GIN index","default":true}}}'::jsonb, CAST('{"jsonb","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataInflection', 'data_inflection', 'data', 'Inflection', 'Transforms field values using inflection operations (snake_case, camelCase, slugify, plural, singular, etc). Attaches BEFORE INSERT and BEFORE UPDATE triggers. References fields by name in data jsonb.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to transform"},"ops":{"type":"array","items":{"type":"string","enum":["plural","singular","camel","pascal","dashed","slugify","underscore","lower","upper"]},"description":"Inflection operations to apply in order"}},"required":["field_name","ops"]}'::jsonb, CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1084,7 +1275,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataLimitCounter', 'data_limit_counter', 'data', 'Limit Counter', 'Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to track (must match a default_limits entry, e.g. \\"projects\\", \\"members\\")"},"scope":{"type":"string","enum":["app","org"],"description":"Limit scope: \\"app\\" (membership_type=1, user-level) or \\"org\\" (membership_type=2, entity-level)","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor or entity id used for limit lookup","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","billing"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataInheritFromParent', 'data_inherit_from_parent', 'data', 'Inherit From Parent', 'BEFORE INSERT trigger that copies specified fields from a parent table via a foreign key. The parent row is looked up through RLS (SECURITY INVOKER), so the insert fails if the caller cannot see the parent. Used by the storage module to inherit owner_id and is_public from buckets to files.', CAST(E'{"type":"object","properties":{"parent_fk_field":{"type":"string","format":"column-ref","description":"Name of the FK field on this table that references the parent (e.g. bucket_id)"},"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to copy from the parent row (e.g. [\\"owner_id\\", \\"is_public\\"])"},"parent_table":{"type":"string","description":"Parent table name (optional fallback if FK not yet registered in metaschema)"},"parent_schema":{"type":"string","description":"Parent table schema (optional, defaults to same schema as child table)"}},"required":["parent_fk_field","fields"]}' AS jsonb), CAST('{"trigger","inheritance","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1095,7 +1292,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataOwnedFields', 'data_owned_fields', 'data', 'Owned Fields', 'Restricts which user can modify specific columns in shared objects. Creates an AFTER UPDATE trigger that throws OWNED_PROPS when a non-owner tries to change protected fields. References fields by name in data jsonb.', CAST('{"type":"object","properties":{"role_key_field_name":{"type":"string","format":"column-ref","description":"Name of the field identifying the owner (e.g. sender_id)"},"protected_field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Names of fields only this owner can modify"}},"required":["role_key_field_name","protected_field_names"]}' AS jsonb), CAST('{"ownership","constraint","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataJsonb', 'data_jsonb', 'data', 'JSONB Field', 'Adds a JSONB column with optional GIN index for containment queries (@>, ?, ?|, ?&). Standard pattern for semi-structured metadata.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the JSONB field","default":"metadata"},"default_value":{"type":"string","description":"Default value expression","default":"''{}''::jsonb"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false},"create_index":{"type":"boolean","description":"Whether to create a GIN index","default":true}}}'::jsonb, CAST('{"jsonb","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1106,7 +1309,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataOwnershipInEntity', 'data_ownership_in_entity', 'data', 'Ownership In Entity', 'Combines direct ownership with entity scoping. Adds both owner_id and entity_id columns. Enables AuthzDirectOwner, AuthzEntityMembership, and AuthzOrgHierarchy authorization. Particularly useful for OrgHierarchy where a user owns a row (owner_id) within an entity (entity_id), and managers above can see subordinate-owned records via the hierarchy closure table.', '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true}}}'::jsonb, CAST('{"ownership","membership","hierarchy","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataOwnedFields', 'data_owned_fields', 'data', 'Owned Fields', 'Restricts which user can modify specific columns in shared objects. Creates an AFTER UPDATE trigger that throws OWNED_PROPS when a non-owner tries to change protected fields. References fields by name in data jsonb.', CAST('{"type":"object","properties":{"role_key_field_name":{"type":"string","format":"column-ref","description":"Name of the field identifying the owner (e.g. sender_id)"},"protected_field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Names of fields only this owner can modify"}},"required":["role_key_field_name","protected_field_names"]}' AS jsonb), CAST('{"ownership","constraint","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1117,7 +1326,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataPeoplestamps', 'data_peoplestamps', 'data', 'Peoplestamps', 'Adds user tracking for creates/updates with created_by and updated_by columns.', '{"type":"object","properties":{"created_by_field":{"type":"string","format":"column-ref","description":"Column name for the creating user reference","default":"created_by"},"updated_by_field":{"type":"string","format":"column-ref","description":"Column name for the last-updating user reference","default":"updated_by"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from created_by and updated_by to the users table","default":false}}}'::jsonb, CAST('{"timestamps","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataOwnershipInEntity', 'data_ownership_in_entity', 'data', 'Ownership In Entity', 'Combines direct ownership with entity scoping. Adds both owner_id and entity_id columns. Enables AuthzDirectOwner, AuthzEntityMembership, and AuthzOrgHierarchy authorization. Particularly useful for OrgHierarchy where a user owns a row (owner_id) within an entity (entity_id), and managers above can see subordinate-owned records via the hierarchy closure table.', '{"type":"object","properties":{"owner_field_name":{"type":"string","format":"column-ref","description":"Column name for the owner reference","default":"owner_id"},"entity_field_name":{"type":"string","format":"column-ref","description":"Column name for the entity reference","default":"entity_id"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from owner_id and entity_id to the users table","default":true},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the owner and entity columns","default":true}}}'::jsonb, CAST('{"ownership","membership","hierarchy","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1128,7 +1343,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataPublishable', 'data_publishable', 'data', 'Publishable', 'Adds publish state columns (is_published, published_at) for content visibility. Enables AuthzPublishable and AuthzTemporal authorization.', '{"type":"object","properties":{"is_published_field":{"type":"string","format":"column-ref","description":"Column name for the published boolean flag","default":"is_published"},"published_at_field":{"type":"string","format":"column-ref","description":"Column name for the publish timestamp","default":"published_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"publishing","temporal","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataPeoplestamps', 'data_peoplestamps', 'data', 'Peoplestamps', 'Adds user tracking for creates/updates with created_by and updated_by columns.', '{"type":"object","properties":{"created_by_field":{"type":"string","format":"column-ref","description":"Column name for the creating user reference","default":"created_by"},"updated_by_field":{"type":"string","format":"column-ref","description":"Column name for the last-updating user reference","default":"updated_by"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true},"include_user_fk":{"type":"boolean","description":"If true, adds foreign key constraints from created_by and updated_by to the users table","default":false},"create_index":{"type":"boolean","description":"If true, creates B-tree indexes on the peoplestamp columns","default":true}}}'::jsonb, CAST('{"timestamps","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1139,7 +1360,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataSlug', 'data_slug', 'data', 'Slug', 'Auto-generates URL-friendly slugs from field values on insert/update. Attaches BEFORE INSERT and BEFORE UPDATE triggers that call inflection.slugify() on the target field. References fields by name in data jsonb.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to slugify"},"source_field_name":{"type":"string","format":"column-ref","description":"Optional source field name (defaults to field_name)"}},"required":["field_name"]}' AS jsonb), CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataPublishable', 'data_publishable', 'data', 'Publishable', 'Adds publish state columns (is_published, published_at) for content visibility. Enables AuthzPublishable and AuthzTemporal authorization.', '{"type":"object","properties":{"is_published_field_name":{"type":"string","format":"column-ref","description":"Column name for the published boolean flag","default":"is_published"},"published_at_field_name":{"type":"string","format":"column-ref","description":"Column name for the publish timestamp","default":"published_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"publishing","temporal","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1150,7 +1377,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataSoftDelete', 'data_soft_delete', 'data', 'Soft Delete', 'Adds soft delete support with deleted_at and is_deleted columns.', '{"type":"object","properties":{"deleted_at_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete timestamp","default":"deleted_at"},"is_deleted_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete boolean flag","default":"is_deleted"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataRealtime', 'data_realtime', 'data', 'Realtime Subscriptions', 'Creates per-table subscriber tables in subscriptions_public with RLS policies derived from source table SELECT policies. Attaches statement-level triggers to emit changes to subscribers.', CAST('{"type":"object","properties":{"operations":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Which DML operations to track with emit_change triggers","default":["INSERT","UPDATE","DELETE"]},"subscriber_table_name":{"type":"string","description":"Custom name for the subscriber table (defaults to {source_table}_subscriber)"}}}' AS jsonb), CAST('{"realtime","subscriptions","triggers"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1161,7 +1394,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataStatusField', 'data_status_field', 'data', 'Status Field', 'Adds a status column with B-tree index for efficient equality filtering and sorting. Optionally constrains values via CHECK constraint when allowed_values is provided.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the status field","default":"status"},"type":{"type":"string","description":"Column type (text or citext)","default":"text"},"default_value":{"type":"string","description":"Default value expression (e.g., active)"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":true},"allowed_values":{"type":"array","items":{"type":"string"},"description":"If provided, creates a CHECK constraint restricting the column to these values"}}}' AS jsonb), CAST('{"status","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataSlug', 'data_slug', 'data', 'Slug', 'Auto-generates URL-friendly slugs from field values on insert/update. Attaches BEFORE INSERT and BEFORE UPDATE triggers that call inflection.slugify() on the target field. References fields by name in data jsonb.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the field to slugify","default":"slug"},"source_field_name":{"type":"string","format":"column-ref","description":"Optional source field name (defaults to field_name)"}},"required":[]}' AS jsonb), CAST('{"transform","behavior"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1172,7 +1411,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataTags', 'data_tags', 'data', 'Tags', 'Adds a citext[] tags column with GIN index for efficient array containment queries (@>, &&). Standard tagging pattern for categorization and filtering.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the tags array","default":"tags"},"default_value":{"type":"string","description":"Default value expression for the tags column"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false}}}'::jsonb, CAST('{"tags","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataSoftDelete', 'data_soft_delete', 'data', 'Soft Delete', 'Adds soft delete support with deleted_at and is_deleted columns.', '{"type":"object","properties":{"deleted_at_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete timestamp","default":"deleted_at"},"is_deleted_field":{"type":"string","format":"column-ref","description":"Column name for the soft-delete boolean flag","default":"is_deleted"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1183,7 +1428,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('DataTimestamps', 'data_timestamps', 'data', 'Timestamps', 'Adds automatic timestamp tracking with created_at and updated_at columns.', '{"type":"object","properties":{"created_at_field":{"type":"string","format":"column-ref","description":"Column name for the creation timestamp","default":"created_at"},"updated_at_field":{"type":"string","format":"column-ref","description":"Column name for the last-updated timestamp","default":"updated_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"timestamps","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataStatusField', 'data_status_field', 'data', 'Status Field', 'Adds a status column with B-tree index for efficient equality filtering and sorting. Optionally constrains values via CHECK constraint when allowed_values is provided.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the status field","default":"status"},"type":{"type":"string","description":"Column type (text or citext)","default":"text"},"default_value":{"type":"string","description":"Default value expression (e.g., active)"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":true},"allowed_values":{"type":"array","items":{"type":"string"},"description":"If provided, creates a CHECK constraint restricting the column to these values"}}}' AS jsonb), CAST('{"status","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1194,7 +1445,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('RelationBelongsTo', 'relation_belongs_to', 'relation', 'Belongs To', 'Creates a foreign key field on the source table referencing the target table. Auto-derives the FK field name from the target table name using inflection (e.g., projects derives project_id). delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field added"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., projects → project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataTags', 'data_tags', 'data', 'Tags', 'Adds a citext[] tags column with GIN index for efficient array containment queries (@>, &&). Standard tagging pattern for categorization and filtering.', '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the tags array","default":"tags"},"default_value":{"type":"string","description":"Default value expression for the tags column","default":"ARRAY[]::citext[]"},"is_required":{"type":"boolean","description":"Whether the column has a NOT NULL constraint","default":false}}}'::jsonb, CAST('{"tags","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1205,7 +1462,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('RelationHasMany', 'relation_has_many', 'relation', 'Has Many', 'Creates a foreign key field on the target table referencing the source table. Inverse of RelationBelongsTo — same FK, different perspective. "projects has many tasks" creates tasks.project_id. Auto-derives the FK field name from the source table name using inflection. delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Parent table being referenced by the FK (e.g., projects in projects has many tasks)"},"target_table_id":{"type":"string","format":"uuid","description":"Child table that receives the FK field (e.g., tasks in projects has many tasks)"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the target table. Auto-derived from source table name if omitted (e.g., projects derives project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","has_many","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('DataTimestamps', 'data_timestamps', 'data', 'Timestamps', 'Adds automatic timestamp tracking with created_at and updated_at columns.', '{"type":"object","properties":{"created_at_field":{"type":"string","format":"column-ref","description":"Column name for the creation timestamp","default":"created_at"},"updated_at_field":{"type":"string","format":"column-ref","description":"Column name for the last-updated timestamp","default":"updated_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, CAST('{"timestamps","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1216,7 +1479,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('RelationHasOne', 'relation_has_one', 'relation', 'Has One', 'Creates a foreign key field with a unique constraint on the source table referencing the target table. Enforces 1:1 cardinality. Auto-derives the FK field name from the target table name using inflection. delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field and unique constraint"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., users → user_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","unique","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('EventReferral', 'event_referral', 'event', 'Event Referral', 'Creates triggers that record events for the referrer (inviter) when their invitees perform actions on a watched table. Resolves the referrer automatically via the invites module''s claimed_invites table using the membership_type context. Supports the same compound condition system as EventTracker. Use with achievements to unlock levels and grant credits based on invitee activity.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record for the referrer (e.g., \\"invitee_uploaded_avatar\\", \\"invitee_completed_onboarding\\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the invitee (actor) ID on the source table — used to look up the referrer via claimed_invites.receiver_id","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped referral events. Omit for user-only events."},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}' AS jsonb), CAST('{"events","referral","invites","analytics","tracking"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1227,7 +1496,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('RelationManyToMany', 'relation_many_to_many', 'relation', 'Many to Many', 'Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"First table in the M:N relationship"},"target_table_id":{"type":"string","format":"uuid","description":"Second table in the M:N relationship"},"junction_table_id":{"type":"string","format":"uuid","description":"Existing junction table to use. If uuid_nil(), a new bare table is created"},"junction_table_name":{"type":"string","description":"Junction table name. Auto-derived from both table names if omitted (e.g., projects + tags derives project_tags)"},"source_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for source table. Auto-derived if omitted (e.g., projects derives project_id)"},"target_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for target table. Auto-derived if omitted (e.g., tags derives tag_id)"},"use_composite_key":{"type":"boolean","description":"When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use nodes with DataId for UUID PK). Mutually exclusive with nodes containing DataId.","default":false},"nodes":{"type":"array","items":{"type":"object"},"description":"Array of node objects for field creation on junction table. Each object has a $type key (e.g. DataId, DataEntityMembership) and optional data keys. Forwarded to secure_table_provision as-is. Empty array means no additional fields."},"grants":{"type":"array","items":{"type":"object","properties":{"roles":{"type":"array","items":{"type":"string"}},"privileges":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["roles","privileges"]},"description":"Unified grant objects for the junction table. Each entry is { roles: string[], privileges: string[][] }. Forwarded to secure_table_provision as-is. Default: []"},"policies":{"type":"array","items":{"type":"object","properties":{"$type":{"type":"string"},"data":{"type":"object"},"privileges":{"type":"array","items":{"type":"string"}},"policy_role":{"type":"string"},"permissive":{"type":"boolean"},"policy_name":{"type":"string"}},"required":["$type"]},"description":"RLS policy objects for the junction table. Each entry has $type (Authz* generator), optional data, privileges, policy_role, permissive, policy_name. Forwarded to secure_table_provision as-is. Default: []"}},"required":["source_table_id","target_table_id"]}' AS jsonb), CAST('{"relation","junction","many_to_many","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('EventTracker', 'event_tracker', 'event', 'Event Tracker', 'Creates triggers that record events via the events module when table rows change. Supports the same compound condition system as JobTrigger (condition_field, watch_fields, or full AND/OR/NOT conditions). Events are recorded to app_events and aggregated automatically. Use with achievements (blueprint-level) to unlock levels and grant credits based on event accumulation.', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"event_name":{"type":"string","description":"Event type name to record (e.g., \\"avatar_uploaded\\", \\"order_completed\\")"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"DML events that trigger recording","default":["INSERT"]},"count":{"type":"integer","description":"Number of events to record per trigger fire","default":1},"toggle":{"type":"boolean","description":"Toggle mode: records event when condition is met, removes when condition is unmet","default":false},"actor_field":{"type":"string","format":"column-ref","description":"Column containing the actor (user) ID to attribute the event to","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column containing the entity ID (org/group) for entity-scoped events. Omit for user-only events."},"auto_register_type":{"type":"boolean","description":"Automatically register the event_name in event_types during provisioning","default":true},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["event_name"]}' AS jsonb), CAST('{"events","triggers","analytics","tracking"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1238,7 +1513,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('RelationSpatial', 'relation_spatial', 'relation', 'Spatial Relation', 'Declares a spatial predicate between two existing geometry/geography columns. Inserts a metaschema_public.spatial_relation row; the sync_spatial_relation_tags trigger then projects a @spatialRelation smart tag onto the owner column so graphile-postgis'' PostgisSpatialRelationsPlugin can expose it as a cross-table filter in GraphQL. Metadata-only: both source_field and target_field must already exist on their tables. Idempotent on (source_table_id, name). One direction per tag — author two RelationSpatial entries if symmetry is desired.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that owns the relation (the @spatialRelation tag is emitted on the owner column of this table)"},"source_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on source_table that carries the @spatialRelation smart tag"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the spatial predicate"},"target_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on target_table that the predicate is evaluated against"},"name":{"type":"string","description":"Relation name (stable, snake_case). Becomes the generated filter field name in GraphQL (e.g. nearby_clinic). Unique per (source_table_id, name) — idempotency key."},"operator":{"type":"string","enum":["st_contains","st_within","st_intersects","st_covers","st_coveredby","st_overlaps","st_touches","st_dwithin"],"description":"PostGIS spatial predicate. One of the 8 whitelisted operators. st_dwithin requires param_name."},"param_name":{"type":"string","description":"Parameter name for parametric operators (currently only st_dwithin, which needs a distance argument). Must be NULL for all other operators. Enforced by table CHECK."}},"required":["source_table_id","source_field_id","target_table_id","target_field_id","name","operator"]}' AS jsonb), CAST('{"relation","spatial","postgis","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('JobTrigger', 'data_job_trigger', 'job', 'Job Trigger', 'Dynamically creates PostgreSQL triggers that enqueue jobs via app_jobs.add_job() when table rows are inserted, updated, or deleted. Supports configurable payload strategies (full row, row ID, selected fields, or custom mapping), conditional firing via WHEN clauses, watched field changes, and extended job options (queue, priority, delay, max attempts).', CAST(E'{"type":"object","$defs":{"triggerCondition":{"type":"object","description":"A leaf condition ({field, op, value?, row?, ref?}) or a combinator ({AND, OR, NOT}).","properties":{"field":{"type":"string","format":"column-ref","description":"Column name (validated against the table)."},"op":{"type":"string","enum":["=","!=",">","<",">=","<=","LIKE","NOT LIKE","IS NULL","IS NOT NULL","IS DISTINCT FROM"],"description":"Comparison operator."},"value":{"description":"Comparison value. Type is resolved from the column definition. Omit for IS NULL, IS NOT NULL, IS DISTINCT FROM."},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW","description":"Row reference (default: NEW)."},"ref":{"type":"object","description":"Column reference for field-to-field comparison (alternative to value).","properties":{"field":{"type":"string","format":"column-ref"},"row":{"type":"string","enum":["NEW","OLD"],"default":"NEW"}}},"AND":{"type":"array","description":"Array of conditions combined with AND.","items":{"$ref":"#/$defs/triggerCondition"}},"OR":{"type":"array","description":"Array of conditions combined with OR.","items":{"$ref":"#/$defs/triggerCondition"}},"NOT":{"$ref":"#/$defs/triggerCondition","description":"Negated condition."}}}},"properties":{"task_identifier":{"type":"string","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe)"},"payload_strategy":{"type":"string","enum":["row","row_id","fields","custom"],"description":"How to build the job payload: row (full NEW/OLD), row_id (just id), fields (selected columns), custom (mapped columns)","default":"row_id"},"payload_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names to include in payload (only for fields strategy)"},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Key-to-column mapping for custom payload (e.g., {\\"invoice_id\\": \\"id\\", \\"total\\": \\"amount\\"})"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE","DELETE"]},"description":"Trigger events to create","default":["INSERT","UPDATE"]},"include_old":{"type":"boolean","description":"Include OLD row in payload (for UPDATE triggers)","default":false},"include_meta":{"type":"boolean","description":"Include table/schema metadata in payload","default":false},"condition_field":{"type":"string","format":"column-ref","description":"Column name for conditional WHEN clause (fires only when field equals condition_value)"},"condition_value":{"type":"string","description":"Value to compare against condition_field in WHEN clause"},"conditions":{"description":"Compound conditions for the trigger WHEN clause. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"},"job_key":{"type":"string","description":"Static job key for upsert semantics (prevents duplicate jobs)"},"queue_name":{"type":"string","description":"Job queue name for routing to specific workers"},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"run_at_delay":{"type":"string","description":"Delay before job runs as PostgreSQL interval (e.g., 30 seconds, 5 minutes)"},"max_attempts":{"type":"integer","description":"Maximum retry attempts for the job","default":25}},"required":["task_identifier"]}' AS jsonb), CAST('{"jobs","triggers","async"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1249,7 +1530,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('SearchBm25', 'search_bm25', 'search', 'BM25 Search', 'Creates a BM25 index on an existing text column using pg_textsearch. Enables statistical relevance ranking with configurable k1 and b parameters. The BM25 index is auto-detected by graphile-search.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of existing text column to index with BM25"},"text_config":{"type":"string","description":"PostgreSQL text search configuration for BM25","default":"english"},"k1":{"type":"number","description":"BM25 k1 parameter: term frequency saturation (typical: 1.2-2.0)","default":null},"b":{"type":"number","description":"BM25 b parameter: document length normalization (0=none, 1=full, typical: 0.75)","default":null},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["field_name"]}' AS jsonb), CAST('{"search","bm25","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('LimitEnforceAggregate', 'limit_enforce_aggregate', 'limit_enforce', 'Aggregate Limit Counter', 'Declaratively attaches aggregate limit-tracking triggers to a table. On INSERT the named limit is incremented per entity; on DELETE it is decremented. Uses org_limit_aggregates_inc/dec for per-entity (org-level) aggregate limits rather than per-user limits. Requires a provisioned limits_module for the target database.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to track (must match a default_limits entry, e.g. \\"databases\\", \\"members\\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for aggregate limit lookup","default":"entity_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","aggregates","billing"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1260,7 +1547,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('SearchFullText', 'search_full_text', 'search', 'Full-Text Search', 'Adds a tsvector column with GIN index and automatic trigger population from source fields. Enables PostgreSQL full-text search with configurable weights and language support. Leverages the existing metaschema full_text_search infrastructure.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"},"lang":{"type":"string","description":"PostgreSQL text search configuration","default":"english"}},"required":["field"]},"description":"Source columns that feed the tsvector. Each has a field name, weight (A-D), and language config."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}' AS jsonb), CAST('{"search","fts","tsvector","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('LimitEnforceCounter', 'limit_enforce_counter', 'limit_enforce', 'Enforce Counter', 'Declaratively attaches limit-tracking triggers to a table. On INSERT the named limit is incremented; on DELETE it is decremented. Requires a provisioned limits_module for the target scope.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to track (must match a default_limits entry, e.g. \\"projects\\", \\"members\\")"},"scope":{"type":"string","enum":["app","org"],"description":"Limit scope: \\"app\\" (membership_type=1, user-level) or \\"org\\" (membership_type=2, entity-level)","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor or entity id used for limit lookup","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","DELETE","UPDATE"]},"description":"Which DML events to attach triggers for","default":["INSERT","DELETE"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","enforce"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1271,7 +1564,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('SearchSpatial', 'search_spatial', 'search', 'Spatial Search', 'Adds a PostGIS geometry or geography column with a spatial index (GiST or SP-GiST). Supports configurable geometry types (Point, Polygon, etc.), SRID, and dimensionality. The graphile-postgis plugin auto-detects geometry/geography columns by codec type for spatial filtering (ST_Contains, ST_DWithin, bbox operators).', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the geometry/geography column","default":"geom"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"PostGIS geometry type constraint","default":"Point"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry (for geodetic calculations on the sphere)","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method","default":"gist"}}}' AS jsonb), CAST('{"spatial","postgis","geometry","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('LimitEnforceFeature', 'limit_enforce_feature', 'limit_enforce', 'Feature Flag', 'Gates a table behind a feature flag backed by the cap tables. Attaches a BEFORE INSERT trigger that checks whether the named feature cap value is > 0. Features are modeled as caps with max=0 (disabled) or max=1 (enabled) in limit_caps / limit_caps_defaults tables. Resolution: COALESCE(per-entity cap, scope default, 0).', CAST(E'{"type":"object","properties":{"feature_name":{"type":"string","description":"Cap name representing this feature (must match a limit_caps_defaults entry with max=0 or max=1)"},"scope":{"type":"string","enum":["app","org"],"description":"Feature scope: \\"app\\" (membership_type=1, app-level caps) or \\"org\\" (membership_type=2, per-entity caps)","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the entity id for per-entity cap lookups (only used for org scope)","default":"entity_id"}},"required":["feature_name"]}' AS jsonb), CAST('{"limits","triggers","feature-flags","billing","caps"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1282,7 +1581,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('SearchSpatialAggregate', 'search_spatial_aggregate', 'search', 'Spatial Aggregate Search', 'Creates a derived/materialized geometry field on the parent table that automatically aggregates geometries from a source (child) table via triggers. When child rows are inserted/updated/deleted, the parent aggregate field is recalculated using the specified PostGIS aggregation function (ST_Union, ST_Collect, ST_ConvexHull, ST_ConcaveHull). Useful for materializing spatial boundaries from collections of points or polygons.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the aggregate geometry column on the parent table","default":"geom_aggregate"},"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source (child) table containing individual geometries"},"source_geom_field":{"type":"string","format":"column-ref","description":"Name of the geometry column on the source table","default":"geom"},"source_fk_field":{"type":"string","format":"column-ref","description":"Name of the foreign key column on the source table pointing to the parent"},"aggregate_function":{"type":"string","enum":["union","collect","convex_hull","concave_hull"],"description":"PostGIS aggregation function: union (ST_Union, merges overlapping), collect (ST_Collect, groups without merging), convex_hull (smallest convex polygon), concave_hull (tighter boundary)","default":"union"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"Output geometry type constraint for the aggregate field","default":"MultiPolygon"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method for the aggregate field","default":"gist"}},"required":["source_table_id","source_fk_field"]}' AS jsonb), CAST('{"spatial","postgis","geometry","aggregate","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('ProcessChunks', 'data_chunks', 'process', 'Chunks', 'Creates a chunked-embedding child table for any parent table. Provisions the chunks table with content, chunk_index, embedding vector, metadata, HNSW index, inherited RLS, and optional job trigger for automatic text splitting. Composed internally by ProcessFileEmbedding (enabled by default in extract mode) but can also be used standalone.', CAST(E'{"type":"object","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"dimensions":{"type":"integer","description":"Vector dimensions for per-chunk embeddings","default":768},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric for the HNSW index on chunk embeddings","default":"cosine"},"chunks_table_name":{"type":"string","description":"Override the chunks table name. Defaults to {parent_table}_chunks."},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from the parent table to copy into chunk metadata"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to create a job trigger that auto-enqueues chunking on parent INSERT/UPDATE","default":true} ,"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"},"embedding_model":{"type":"string","description":"Embedding model identifier for per-chunk embeddings. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."}}}' AS jsonb), CAST('{"embedding","chunks","vector","ai","rag"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1293,7 +1598,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('SearchTrgm', 'search_trgm', 'search', 'Trigram Search', 'Creates GIN trigram indexes (gin_trgm_ops) on specified text/citext fields for fuzzy LIKE/ILIKE/similarity search. Adds @trgmSearch smart tag for PostGraphile integration. Fields must already exist on the table.', CAST('{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to create trigram indexes on (fields must already exist on the table)"}},"required":["fields"]}' AS jsonb), CAST('{"search","trigram","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('ProcessExtraction', 'process_extraction', 'process', 'File Extraction', 'Creates extraction output fields and a job trigger for file text extraction. Fires when a file is uploaded (status = ''uploaded'') or on INSERT. The external worker extracts text/metadata from the file (PDF, DOCX, HTML, etc.) and writes the result back to the configured output fields. Typically used upstream of ProcessFileEmbedding or ProcessChunks.', CAST(E'{"type":"object","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''application/pdf'', ''text/%''], [''application/vnd.openxmlformats%''].","default":["application/pdf","text/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the extraction worker","default":"extract_file_text"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks (e.g., status = ''uploaded'').","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"queue_name":{"type":"string","description":"Job queue name for extraction tasks","default":"extraction"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5} ,"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0},"extraction_model":{"type":"string","description":"Extraction model identifier (e.g. a vision model for OCR, an LLM for structured extraction). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config."},"extraction_provider":{"type":"string","description":"Extraction provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."}}}' AS jsonb), CAST('{"extraction","files","processing","jobs","text"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1304,7 +1615,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('SearchUnified', 'search_unified', 'search', 'Unified Search', 'Composite node type that orchestrates multiple search modalities (full-text search, BM25, embeddings, trigram) on a single table. Configures per-table search score weights, normalization strategy, and recency boost via the @searchConfig smart tag.', CAST('{"type":"object","properties":{"full_text_search":{"type":"object","description":"SearchFullText parameters. Omit to skip FTS setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref"},"weight":{"type":"string","enum":["A","B","C","D"]},"lang":{"type":"string"}},"required":["field"]}},"search_score_weight":{"type":"number","default":1}}},"bm25":{"type":"object","description":"SearchBm25 parameters. Omit to skip BM25 setup.","properties":{"field_name":{"type":"string","format":"column-ref"},"text_config":{"type":"string","default":"english"},"k1":{"type":"number"},"b":{"type":"number"},"search_score_weight":{"type":"number","default":1}}},"embedding":{"type":"object","description":"SearchVector parameters. Omit to skip embedding setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"embedding"},"dimensions":{"type":"integer","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"]},"metric":{"type":"string","enum":["cosine","l2","ip"]},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"}},"search_score_weight":{"type":"number","default":1},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}},"trgm_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to tag with @trgmSearch for fuzzy/typo-tolerant matching"},"search_config":{"type":"object","description":"Unified search score configuration written to @searchConfig smart tag","properties":{"weights":{"type":"object","description":"Per-algorithm weights: {tsv: 1.5, bm25: 1.0, pgvector: 0.8, trgm: 0.3}"},"normalization":{"type":"string","enum":["linear","sigmoid"],"description":"Score normalization strategy","default":"linear"},"boost_recent":{"type":"boolean","description":"Enable recency boost for search results","default":false},"boost_recency_field":{"type":"string","format":"column-ref","description":"Timestamp field for recency boost (e.g. created_at, updated_at)"},"boost_recency_decay":{"type":"number","description":"Decay rate for recency boost (0-1, lower = faster decay)","default":0.5}}}}}' AS jsonb), CAST('{"search","composite","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('ProcessFileEmbedding', 'data_file_embedding', 'process', 'File Embedding', 'Generic, MIME-scoped embedding node for file tables. Supports two modes: direct (whole-file to single vector, e.g. CLIP for images) when extraction is omitted, or extract (file to text to chunks to per-chunk vectors) when extraction config is provided. Composes SearchVector + JobTrigger + ProcessChunks (enabled by default in extract mode) internally. Multiple instances can coexist on the same table with different MIME scopes, field names, and embedding strategies.', CAST(E'{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 512 for CLIP, 768 for nomic, 1536 for ada-002)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together. Examples: [''image/%''], [''application/pdf'', ''text/%''], [''audio/%''].","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the worker. In direct mode this is the embedding worker; in extract mode this is the extraction worker.","default":"process_file_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND. Use this to add status checks, field guards, etc.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"extraction":{"type":"object","description":"Text extraction configuration. When present, the generator creates extraction output fields on the table and configures SearchVector with source_fields + stale tracking. When absent, the node operates in direct mode (single vector per file, no text extraction).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text/markdown","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata (page count, language, etc.)","default":"extracted_metadata"}}},"include_chunks":{"type":"boolean","description":"Whether to create a chunks table via ProcessChunks. Defaults to true when extraction is provided, false in direct mode. Set explicitly to override."},"chunks":{"type":"object","description":"Chunking configuration passed through to ProcessChunks. When include_chunks is true (or defaults to true in extract mode), these params configure the chunks table, embedding dimensions, strategy, etc.","default":{},"properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"paragraph"},"metadata_fields":{"type":"array","items":{"type":"string"},"description":"Field names from parent to copy into chunk metadata"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true} ,"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \\"nomic-embed-text\\", \\"text-embedding-3-small\\", \\"clip-vit-base-patch32\\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."}}}' AS jsonb), CAST('{"embedding","vector","ai","composition","jobs","multimodal","files"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1315,7 +1632,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('SearchVector', 'search_vector', 'search', 'Vector Search', 'Adds a vector embedding column with HNSW or IVFFlat index for similarity search. Supports configurable dimensions, distance metrics (cosine, l2, ip), stale tracking strategies (column, null, hash), and automatic job enqueue triggers for embedding generation.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 384, 768, 1536, 3072)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric (cosine, l2, ip)","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"include_stale_field":{"type":"boolean","description":"When stale_strategy is column, adds an embedding_stale boolean field","default":true},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names that feed the embedding. Used by stale trigger to detect content changes."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","description":"Task identifier for the job queue","default":"generate_embedding"},"stale_strategy":{"type":"string","enum":["column","null","hash"],"description":"Strategy for tracking embedding staleness. column: embedding_stale boolean. null: set embedding to NULL. hash: md5 hash of source fields.","default":"column"},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}}}}' AS jsonb), CAST('{"embedding","vector","ai","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('ProcessImageEmbedding', 'data_image_embedding', 'process', 'Image Embedding', 'Image-specific preset of ProcessFileEmbedding. Delegates to ProcessFileEmbedding with image-oriented defaults: dimensions=512 (CLIP), mime_patterns=[''image/%''], task_identifier=''process_image_embedding'', direct mode (no extraction). Accepts all ProcessFileEmbedding parameters — any overrides are forwarded through.', CAST(E'{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector embedding column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (default 512 for CLIP-style image embeddings)","default":512},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Multiple patterns are OR''d together.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image embedding worker","default":"process_image_embedding"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"extraction":{"type":"object","description":"Text extraction configuration. Forwarded to ProcessFileEmbedding. When present, enables extract mode (e.g., OCR for images).","properties":{"text_field":{"type":"string","format":"column-ref","description":"Field to store extracted text","default":"extracted_text"},"metadata_field":{"type":"string","format":"column-ref","description":"JSONB field for extraction metadata","default":"extracted_metadata"}}},"chunks":{"type":"object","description":"Chunking configuration. Forwarded to ProcessFileEmbedding. Only meaningful when extraction is also provided.","properties":{"content_field_name":{"type":"string","format":"column-ref","default":"content"},"chunk_size":{"type":"integer","default":1000},"chunk_overlap":{"type":"integer","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"default":"paragraph"},"metadata_fields":{"type":"object"},"enqueue_chunking_job":{"type":"boolean","default":true} ,"chunking_task_name":{"type":"string","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \\"clip-vit-base-patch32\\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."}}}' AS jsonb), CAST('{"embedding","image","vector","ai","composition","jobs"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1326,7 +1649,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('TableOrganizationSettings', 'table_organization_settings', 'data', 'Organization Settings', 'Creates an organization settings table with standard business fields (legal_name, address fields). Uses AuthzEntityMembership for access control.', '{"type":"object","properties":{}}'::jsonb, CAST('{"template","settings","membership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('ProcessImageVersions', 'process_image_versions', 'process', 'Image Versions', 'Creates a job trigger for image variant generation. Fires when an image file is uploaded (status = ''uploaded'') or on INSERT. The external worker generates resized, cropped, or reformatted versions (thumbnails, previews, WebP conversions, etc.) and stores them as new file records linked to the source image.', CAST(E'{"type":"object","required":["versions"],"properties":{"versions":{"type":"array","items":{"type":"object","properties":{"name":{"type":"string","description":"Version identifier (e.g., \\"thumb\\", \\"preview\\", \\"hero\\")"},"width":{"type":"integer","description":"Target width in pixels"},"height":{"type":"integer","description":"Target height in pixels"},"fit":{"type":"string","enum":["cover","contain","fill","inside","outside"],"description":"Resize fitting strategy","default":"cover"},"format":{"type":"string","enum":["jpeg","png","webp","avif"],"description":"Output image format","default":"webp"},"quality":{"type":"integer","description":"Output quality (1-100)","default":80}},"required":["name"]},"description":"Array of version definitions. Each version specifies dimensions, format, and quality for a generated image variant. Required — the blueprint must explicitly define what variants to generate.","minItems":1},"mime_patterns":{"type":"array","items":{"type":"string"},"description":"MIME type LIKE patterns to match. Defaults to all image types.","default":["image/%"]},"task_identifier":{"type":"string","description":"Job task identifier for the image processing worker","default":"process_image_versions"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Trigger events that fire the job","default":["INSERT"]},"payload_custom":{"type":"object","additionalProperties":{"type":"string","format":"column-ref"},"description":"Custom payload key-to-column mapping for the job trigger","default":{"file_id":"id","key":"key","mime_type":"mime_type","bucket_id":"bucket_id"}},"trigger_conditions":{"description":"Additional compound conditions beyond MIME filtering. Merged with the auto-generated MIME conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"queue_name":{"type":"string","description":"Job queue name for image processing tasks","default":"image_processing"},"max_attempts":{"type":"integer","description":"Maximum number of retry attempts","default":5},"priority":{"type":"integer","description":"Job priority (lower = higher priority)","default":0}}}' AS jsonb), CAST('{"images","processing","jobs","resize","thumbnails","files"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1337,7 +1666,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('TableUserProfiles', 'table_user_profiles', 'data', 'User Profiles', 'Creates a user profiles table with standard profile fields (profile_picture, bio, first_name, last_name, tags, desired). Uses AuthzDirectOwner for edit access and AuthzAllowAll for select.', '{"type":"object","properties":{}}'::jsonb, CAST('{"template","settings","ownership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('RelationBelongsTo', 'relation_belongs_to', 'relation', 'Belongs To', 'Creates a foreign key field on the source table referencing the target table. Auto-derives the FK field name from the target table name using inflection (e.g., projects derives project_id). delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field added"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., projects → project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1348,7 +1683,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('TableUserSettings', 'table_user_settings', 'data', 'User Settings', 'Creates a user settings table for user-specific configuration. Uses AuthzDirectOwner for access control.', '{"type":"object","properties":{}}'::jsonb, CAST('{"template","settings","ownership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('RelationHasMany', 'relation_has_many', 'relation', 'Has Many', 'Creates a foreign key field on the target table referencing the source table. Inverse of RelationBelongsTo — same FK, different perspective. "projects has many tasks" creates tasks.project_id. Auto-derives the FK field name from the source table name using inflection. delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Parent table being referenced by the FK (e.g., projects in projects has many tasks)"},"target_table_id":{"type":"string","format":"uuid","description":"Child table that receives the FK field (e.g., tasks in projects has many tasks)"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the target table. Auto-derived from source table name if omitted (e.g., projects derives project_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","has_many","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1359,7 +1700,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('ViewAggregated', 'view_aggregated', 'view', 'Aggregated View', 'View with GROUP BY and aggregate functions. Useful for summary/reporting views.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"group_by_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to group by"},"aggregates":{"type":"array","items":{"type":"object","properties":{"function":{"type":"string","enum":["COUNT","SUM","AVG","MIN","MAX"]},"field":{"type":"string","format":"column-ref","description":"Field to aggregate (or * for COUNT)"},"alias":{"type":"string","format":"column-ref","description":"Output column name"}},"required":["function","alias"]},"description":"Array of aggregate specifications"}},"required":["source_table_id","group_by_fields","aggregates"]}' AS jsonb), CAST('{"view","aggregate","reporting"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('RelationHasOne', 'relation_has_one', 'relation', 'Has One', 'Creates a foreign key field with a unique constraint on the source table referencing the target table. Enforces 1:1 cardinality. Auto-derives the FK field name from the target table name using inflection. delete_action is required and must be explicitly provided by the caller.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that will have the FK field and unique constraint"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the FK"},"field_name":{"type":"string","format":"column-ref","description":"FK field name on the source table. Auto-derived from target table name if omitted (e.g., users → user_id)"},"delete_action":{"type":"string","enum":["c","r","n","d","a"],"description":"FK delete action: c=CASCADE, r=RESTRICT, n=SET NULL, d=SET DEFAULT, a=NO ACTION. Required."},"is_required":{"type":"boolean","description":"Whether the FK field is NOT NULL","default":true}},"required":["source_table_id","target_table_id","delete_action"]}' AS jsonb), CAST('{"relation","foreign_key","unique","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1370,7 +1717,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('ViewComposite', 'view_composite', 'view', 'Composite View', 'Advanced view using composite AST for the query. Use when other node types are insufficient (CTEs, UNIONs, complex subqueries, etc.).', CAST('{"type":"object","properties":{"query_ast":{"type":"object","description":"Composite SELECT query AST (JSONB)"}},"required":["query_ast"]}' AS jsonb), CAST('{"view","advanced","composite","ast"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('RelationManyToMany', 'relation_many_to_many', 'relation', 'Many to Many', 'Creates a junction table between source and target tables with auto-derived naming and FK fields. The trigger creates a bare table (no implicit DataId), adds FK fields to both tables, optionally creates a composite PK (use_composite_key), then forwards all security config to secure_table_provision as-is. The trigger never injects values the caller did not provide. Junction table FKs always CASCADE on delete.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"First table in the M:N relationship"},"target_table_id":{"type":"string","format":"uuid","description":"Second table in the M:N relationship"},"junction_table_id":{"type":"string","format":"uuid","description":"Existing junction table to use. If uuid_nil(), a new bare table is created"},"junction_table_name":{"type":"string","description":"Junction table name. Auto-derived from both table names if omitted (e.g., projects + tags derives project_tags)"},"source_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for source table. Auto-derived if omitted (e.g., projects derives project_id)"},"target_field_name":{"type":"string","format":"column-ref","description":"FK field name on junction for target table. Auto-derived if omitted (e.g., tags derives tag_id)"},"use_composite_key":{"type":"boolean","description":"When true, creates a composite PK from the two FK fields. When false, no PK is created by the trigger (use nodes with DataId for UUID PK). Mutually exclusive with nodes containing DataId.","default":false},"nodes":{"type":"array","items":{"type":"object"},"description":"Array of node objects for field creation on junction table. Each object has a $type key (e.g. DataId, DataEntityMembership) and optional data keys. Forwarded to secure_table_provision as-is. Empty array means no additional fields."},"grants":{"type":"array","items":{"type":"object","properties":{"roles":{"type":"array","items":{"type":"string"}},"privileges":{"type":"array","items":{"type":"array","items":{"type":"string"}}}},"required":["roles","privileges"]},"description":"Unified grant objects for the junction table. Each entry is { roles: string[], privileges: string[][] }. Forwarded to secure_table_provision as-is. Default: []"},"policies":{"type":"array","items":{"type":"object","properties":{"$type":{"type":"string"},"data":{"type":"object"},"privileges":{"type":"array","items":{"type":"string"}},"policy_role":{"type":"string"},"permissive":{"type":"boolean"},"policy_name":{"type":"string"}},"required":["$type"]},"description":"RLS policy objects for the junction table. Each entry has $type (Authz* generator), optional data, privileges, policy_role, permissive, policy_name. Forwarded to secure_table_provision as-is. Default: []"}},"required":["source_table_id","target_table_id"]}' AS jsonb), CAST('{"relation","junction","many_to_many","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1381,7 +1734,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('ViewFilteredTable', 'view_filtered_table', 'view', 'Filtered Table', 'Table projection with an Authz* filter baked into the view definition. The view only returns records matching the filter.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"filter_type":{"type":"string","description":"Authz* node type name (e.g., AuthzDirectOwner, AuthzPublishable)"},"filter_data":{"type":"object","description":"Parameters for the Authz* filter type"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to field_names)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id","filter_type"]}' AS jsonb), CAST('{"view","filter","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('RelationSpatial', 'relation_spatial', 'relation', 'Spatial Relation', 'Declares a spatial predicate between two existing geometry/geography columns. Inserts a metaschema_public.spatial_relation row; the sync_spatial_relation_tags trigger then projects a @spatialRelation smart tag onto the owner column so graphile-postgis'' PostgisSpatialRelationsPlugin can expose it as a cross-table filter in GraphQL. Metadata-only: both source_field and target_field must already exist on their tables. Idempotent on (source_table_id, name). One direction per tag — author two RelationSpatial entries if symmetry is desired.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"Table that owns the relation (the @spatialRelation tag is emitted on the owner column of this table)"},"source_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on source_table that carries the @spatialRelation smart tag"},"target_table_id":{"type":"string","format":"uuid","description":"Table being referenced by the spatial predicate"},"target_field_id":{"type":"string","format":"uuid","description":"Geometry/geography column on target_table that the predicate is evaluated against"},"name":{"type":"string","description":"Relation name (stable, snake_case). Becomes the generated filter field name in GraphQL (e.g. nearby_clinic). Unique per (source_table_id, name) — idempotency key."},"operator":{"type":"string","enum":["st_contains","st_within","st_intersects","st_covers","st_coveredby","st_overlaps","st_touches","st_dwithin"],"description":"PostGIS spatial predicate. One of the 8 whitelisted operators. st_dwithin requires param_name."},"param_name":{"type":"string","description":"Parameter name for parametric operators (currently only st_dwithin, which needs a distance argument). Must be NULL for all other operators. Enforced by table CHECK."}},"required":["source_table_id","source_field_id","target_table_id","target_field_id","name","operator"]}' AS jsonb), CAST('{"relation","spatial","postgis","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1392,7 +1751,13 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('ViewJoinedTables', 'view_joined_tables', 'view', 'Joined Tables', 'View that joins multiple tables together. Supports INNER, LEFT, RIGHT, and FULL joins.', CAST('{"type":"object","properties":{"primary_table_id":{"type":"string","format":"uuid","description":"UUID of the primary (left-most) table"},"primary_columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of column names to include from the primary table"},"joins":{"type":"array","items":{"type":"object","properties":{"table_id":{"type":"string","format":"uuid","description":"UUID of the joined table"},"join_type":{"type":"string","enum":["INNER","LEFT","RIGHT","FULL"]},"primary_field":{"type":"string","format":"column-ref","description":"Field on primary table"},"join_field":{"type":"string","format":"column-ref","description":"Field on joined table"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional column names to include from this joined table"}},"required":["table_id","primary_field","join_field"]},"description":"Array of join specifications"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to per-table columns)"}},"required":["primary_table_id","joins"]}' AS jsonb), CAST('{"view","join"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; + ('SearchBm25', 'search_bm25', 'search', 'BM25 Search', 'Creates a BM25 index on an existing text column using pg_textsearch. Enables statistical relevance ranking with configurable k1 and b parameters. The BM25 index is auto-detected by graphile-search.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of existing text column to index with BM25"},"text_config":{"type":"string","description":"PostgreSQL text search configuration for BM25","default":"english"},"k1":{"type":"number","description":"BM25 k1 parameter: term frequency saturation (typical: 1.2-2.0)","default":null},"b":{"type":"number","description":"BM25 b parameter: document length normalization (0=none, 1=full, typical: 0.75)","default":null},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["field_name"]}' AS jsonb), CAST('{"search","bm25","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; INSERT INTO metaschema_public.node_type_registry ( name, @@ -1403,4 +1768,251 @@ INSERT INTO metaschema_public.node_type_registry ( parameter_schema, tags ) VALUES - ('ViewTableProjection', 'view_table_projection', 'view', 'Table Projection', 'Simple column selection from a single source table. Projects all or specific fields.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table to project from"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (all fields if omitted)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id"]}' AS jsonb), CAST('{"view","projection"}' AS text[])) ON CONFLICT (name) DO UPDATE SET slug = excluded.slug, category = excluded.category, display_name = excluded.display_name, description = excluded.description, parameter_schema = excluded.parameter_schema, tags = excluded.tags; \ No newline at end of file + ('SearchFullText', 'search_full_text', 'search', 'Full-Text Search', 'Adds a tsvector column with GIN index and automatic trigger population from source fields. Enables PostgreSQL full-text search with configurable weights and language support. Leverages the existing metaschema full_text_search infrastructure.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"},"lang":{"type":"string","description":"PostgreSQL text search configuration","default":"english"}},"required":["field"]},"description":"Source columns that feed the tsvector. Each has a field name, weight (A-D), and language config."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}' AS jsonb), CAST('{"search","fts","tsvector","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchSpatial', 'search_spatial', 'search', 'Spatial Search', 'Adds a PostGIS geometry or geography column with a spatial index (GiST or SP-GiST). Supports configurable geometry types (Point, Polygon, etc.), SRID, and dimensionality. The graphile-postgis plugin auto-detects geometry/geography columns by codec type for spatial filtering (ST_Contains, ST_DWithin, bbox operators).', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the geometry/geography column","default":"geom"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"PostGIS geometry type constraint","default":"Point"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry (for geodetic calculations on the sphere)","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method","default":"gist"}}}' AS jsonb), CAST('{"spatial","postgis","geometry","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchSpatialAggregate', 'search_spatial_aggregate', 'search', 'Spatial Aggregate Search', 'Creates a derived/materialized geometry field on the parent table that automatically aggregates geometries from a source (child) table via triggers. When child rows are inserted/updated/deleted, the parent aggregate field is recalculated using the specified PostGIS aggregation function (ST_Union, ST_Collect, ST_ConvexHull, ST_ConcaveHull). Useful for materializing spatial boundaries from collections of points or polygons.', CAST('{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the aggregate geometry column on the parent table","default":"geom_aggregate"},"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source (child) table containing individual geometries"},"source_geom_field":{"type":"string","format":"column-ref","description":"Name of the geometry column on the source table","default":"geom"},"source_fk_field":{"type":"string","format":"column-ref","description":"Name of the foreign key column on the source table pointing to the parent"},"aggregate_function":{"type":"string","enum":["union","collect","convex_hull","concave_hull"],"description":"PostGIS aggregation function: union (ST_Union, merges overlapping), collect (ST_Collect, groups without merging), convex_hull (smallest convex polygon), concave_hull (tighter boundary)","default":"union"},"geometry_type":{"type":"string","enum":["Point","LineString","Polygon","MultiPoint","MultiLineString","MultiPolygon","GeometryCollection","Geometry"],"description":"Output geometry type constraint for the aggregate field","default":"MultiPolygon"},"srid":{"type":"integer","description":"Spatial Reference System Identifier (e.g. 4326 for WGS84)","default":4326},"dimension":{"type":"integer","enum":[2,3,4],"description":"Coordinate dimension (2=XY, 3=XYZ, 4=XYZM)","default":2},"use_geography":{"type":"boolean","description":"Use geography type instead of geometry","default":false},"index_method":{"type":"string","enum":["gist","spgist"],"description":"Spatial index method for the aggregate field","default":"gist"}},"required":["source_table_id","source_fk_field"]}' AS jsonb), CAST('{"spatial","postgis","geometry","aggregate","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchTrgm', 'search_trgm', 'search', 'Trigram Search', 'Creates GIN trigram indexes (gin_trgm_ops) on specified text/citext fields for fuzzy LIKE/ILIKE/similarity search. Adds @trgmSearch smart tag for PostGraphile integration. Fields must already exist on the table.', CAST('{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to create trigram indexes on (fields must already exist on the table)"}},"required":["fields"]}' AS jsonb), CAST('{"search","trigram","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchUnified', 'search_unified', 'search', 'Unified Search', 'Composite node type that orchestrates multiple search modalities (full-text search, BM25, embeddings, trigram) on a single table. Configures per-table search score weights, normalization strategy, and recency boost via the @searchConfig smart tag.', CAST('{"type":"object","properties":{"full_text_search":{"type":"object","description":"SearchFullText parameters. Omit to skip FTS setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref"},"weight":{"type":"string","enum":["A","B","C","D"]},"lang":{"type":"string"}},"required":["field"]}},"search_score_weight":{"type":"number","default":1}}},"bm25":{"type":"object","description":"SearchBm25 parameters. Omit to skip BM25 setup.","properties":{"field_name":{"type":"string","format":"column-ref"},"text_config":{"type":"string","default":"english"},"k1":{"type":"number"},"b":{"type":"number"},"search_score_weight":{"type":"number","default":1}}},"embedding":{"type":"object","description":"SearchVector parameters. Omit to skip embedding setup.","properties":{"field_name":{"type":"string","format":"column-ref","default":"embedding"},"dimensions":{"type":"integer","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"]},"metric":{"type":"string","enum":["cosine","l2","ip"]},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"}},"search_score_weight":{"type":"number","default":1},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier. When null, the worker falls back to runtime config."},"embedding_provider":{"type":"string","description":"Embedding provider name. When null, the worker falls back to runtime config."}}},"embedding_text_field":{"type":"string","format":"column-ref","description":"Name of the composite text field created for embedding input","default":"embedding_text"},"composite_format":{"type":"string","enum":["labeled","plain"],"description":"Output format for the composite text field","default":"labeled"},"trgm_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to tag with @trgmSearch for fuzzy/typo-tolerant matching"},"search_config":{"type":"object","description":"Unified search score configuration written to @searchConfig smart tag","properties":{"weights":{"type":"object","description":"Per-algorithm weights: {tsv: 1.5, bm25: 1.0, pgvector: 0.8, trgm: 0.3}"},"normalization":{"type":"string","enum":["linear","sigmoid"],"description":"Score normalization strategy","default":"linear"},"boost_recent":{"type":"boolean","description":"Enable recency boost for search results","default":false},"boost_recency_field":{"type":"string","format":"column-ref","description":"Timestamp field for recency boost (e.g. created_at, updated_at)"},"boost_recency_decay":{"type":"number","description":"Decay rate for recency boost (0-1, lower = faster decay)","default":0.5}}}}}' AS jsonb), CAST('{"search","composite","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('SearchVector', 'search_vector', 'search', 'Vector Search', 'Adds a vector embedding column with HNSW or IVFFlat index for similarity search. Supports configurable dimensions, distance metrics (cosine, l2, ip), per-field {field_name}_updated_at timestamp tracking (read-only in GraphQL), and automatic job enqueue triggers for embedding generation.', CAST(E'{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the vector column","default":"embedding"},"dimensions":{"type":"integer","description":"Vector dimensions (e.g. 384, 768, 1536, 3072)","default":768},"index_method":{"type":"string","enum":["hnsw","ivfflat"],"description":"Index type for similarity search","default":"hnsw"},"metric":{"type":"string","enum":["cosine","l2","ip"],"description":"Distance metric (cosine, l2, ip)","default":"cosine"},"index_options":{"type":"object","description":"Index-specific options. HNSW: {m, ef_construction}. IVFFlat: {lists}.","default":{}},"source_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Column names that feed the embedding. Used by stale trigger to detect content changes."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","description":"Task identifier for the job queue","default":"generate_embedding"},"chunks":{"type":"object","description":"Chunking configuration for long-text embedding. Creates an embedding_chunks record that drives automatic text splitting and per-chunk embedding. Omit to skip chunking.","properties":{"content_field_name":{"type":"string","format":"column-ref","description":"Name of the text content column in the chunks table","default":"content"},"chunk_size":{"type":"integer","description":"Maximum number of characters per chunk","default":1000},"chunk_overlap":{"type":"integer","description":"Number of overlapping characters between consecutive chunks","default":200},"chunk_strategy":{"type":"string","enum":["fixed","sentence","paragraph","semantic"],"description":"Strategy for splitting text into chunks","default":"fixed"},"metadata_fields":{"type":"object","description":"Metadata fields from parent to copy into chunks"},"enqueue_chunking_job":{"type":"boolean","description":"Whether to auto-enqueue a chunking job on insert/update","default":true},"chunking_task_name":{"type":"string","description":"Task identifier for the chunking job queue","default":"generate_chunks"}}},"embedding_model":{"type":"string","description":"Embedding model identifier (e.g. \\"nomic-embed-text\\", \\"text-embedding-3-small\\"). Included in the job payload so the worker knows which model to use. When null, the worker falls back to runtime config (llm_module / env vars)."},"embedding_provider":{"type":"string","description":"Embedding provider name (e.g. \\"ollama\\", \\"openai\\"). When null, the worker falls back to runtime config."}}}' AS jsonb), CAST('{"embedding","vector","ai","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('TableOrganizationSettings', 'table_organization_settings', 'data', 'Organization Settings', 'Creates an organization settings table with standard business fields (legal_name, address fields). Uses AuthzEntityMembership for access control.', '{"type":"object","properties":{}}'::jsonb, CAST('{"template","settings","membership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('TableUserProfiles', 'table_user_profiles', 'data', 'User Profiles', 'Creates a user profiles table with standard profile fields (profile_picture, bio, first_name, last_name, tags, desired). Uses AuthzDirectOwner for edit access and AuthzAllowAll for select.', '{"type":"object","properties":{}}'::jsonb, CAST('{"template","settings","ownership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('TableUserSettings', 'table_user_settings', 'data', 'User Settings', 'Creates a user settings table for user-specific configuration. Uses AuthzDirectOwner for access control.', '{"type":"object","properties":{}}'::jsonb, CAST('{"template","settings","ownership","schema"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewAggregated', 'view_aggregated', 'view', 'Aggregated View', 'View with GROUP BY and aggregate functions. Useful for summary/reporting views.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"group_by_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names to group by"},"aggregates":{"type":"array","items":{"type":"object","properties":{"function":{"type":"string","enum":["COUNT","SUM","AVG","MIN","MAX"]},"field":{"type":"string","format":"column-ref","description":"Field to aggregate (or * for COUNT)"},"alias":{"type":"string","format":"column-ref","description":"Output column name"}},"required":["function","alias"]},"description":"Array of aggregate specifications"}},"required":["source_table_id","group_by_fields","aggregates"]}' AS jsonb), CAST('{"view","aggregate","reporting"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewComposite', 'view_composite', 'view', 'Composite View', 'Advanced view using composite AST for the query. Use when other node types are insufficient (CTEs, UNIONs, complex subqueries, etc.).', CAST('{"type":"object","properties":{"query_ast":{"type":"object","description":"Composite SELECT query AST (JSONB)"}},"required":["query_ast"]}' AS jsonb), CAST('{"view","advanced","composite","ast"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewFilteredTable', 'view_filtered_table', 'view', 'Filtered Table', 'Table projection with an Authz* filter baked into the view definition. The view only returns records matching the filter.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table"},"filter_type":{"type":"string","description":"Authz* node type name (e.g., AuthzDirectOwner, AuthzPublishable)"},"filter_data":{"type":"object","description":"Parameters for the Authz* filter type"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to field_names)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id","filter_type"]}' AS jsonb), CAST('{"view","filter","authz"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewJoinedTables', 'view_joined_tables', 'view', 'Joined Tables', 'View that joins multiple tables together. Supports INNER, LEFT, RIGHT, and FULL joins.', CAST('{"type":"object","properties":{"primary_table_id":{"type":"string","format":"uuid","description":"UUID of the primary (left-most) table"},"primary_columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of column names to include from the primary table"},"joins":{"type":"array","items":{"type":"object","properties":{"table_id":{"type":"string","format":"uuid","description":"UUID of the joined table"},"join_type":{"type":"string","enum":["INNER","LEFT","RIGHT","FULL"]},"primary_field":{"type":"string","format":"column-ref","description":"Field on primary table"},"join_field":{"type":"string","format":"column-ref","description":"Field on joined table"},"columns":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional column names to include from this joined table"}},"required":["table_id","primary_field","join_field"]},"description":"Array of join specifications"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (alternative to per-table columns)"}},"required":["primary_table_id","joins"]}' AS jsonb), CAST('{"view","join"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('ViewTableProjection', 'view_table_projection', 'view', 'Table Projection', 'Simple column selection from a single source table. Projects all or specific fields.', CAST('{"type":"object","properties":{"source_table_id":{"type":"string","format":"uuid","description":"UUID of the source table to project from"},"field_ids":{"type":"array","items":{"type":"string","format":"uuid"},"description":"Optional array of field UUIDs to include (all fields if omitted)"},"field_names":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Optional array of field names to include (alternative to field_ids)"}},"required":["source_table_id"]}' AS jsonb), CAST('{"view","projection"}' AS text[])) ON CONFLICT (name) DO UPDATE SET + slug = excluded.slug, + category = excluded.category, + display_name = excluded.display_name, + description = excluded.description, + parameter_schema = excluded.parameter_schema, + tags = excluded.tags; + +CREATE TABLE metaschema_public.function ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + 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, + UNIQUE (schema_id, name) +); + +CREATE INDEX function_database_id_idx ON metaschema_public.function (database_id); + +CREATE INDEX function_schema_id_idx ON metaschema_public.function (schema_id); \ No newline at end of file diff --git a/packages/object-store/README.md b/packages/object-store/README.md index 2b46009f9..1d882f624 100644 --- a/packages/object-store/README.md +++ b/packages/object-store/README.md @@ -12,28 +12,19 @@

-A content-addressable, immutable object store for PostgreSQL — Merkle trees, all the way down. Pair with [`@pgpm/object-tree`](https://www.npmjs.com/package/@pgpm/object-tree) for Git-like version control. +Immutable versioned object storage with content-addressable IDs for PostgreSQL ## Overview -Born from the conviction that the best ideas in computer science keep getting reinvented because they keep being right, `@pgpm/object-store` brings content-addressable storage and structural sharing into PostgreSQL. Inspired by Rich Hickey's philosophy that immutability is not a constraint but a liberation — that the past should never be silently rewritten — this package implements a Merkle tree directly in SQL, where every object's identity is derived from what it contains rather than where it sits. - -The original code was written in Egypt, during a period of deep focus on what it would mean to treat a relational database the way Hickey's Datomic treats time: as an accretion of immutable facts rather than a place that forgets. The result is a storage engine where objects are permanent, identity is derived from content, and every version of every tree is always available — not because you remembered to snapshot it, but because the data structure itself makes forgetting impossible. - -The design follows a simple principle borrowed from Git's object model: if you know the content, you know the identity. Objects are stored as nodes in a tree. Each node holds a JSON payload and an ordered list of named children. A trigger hashes the content on every insert, producing a deterministic UUID. Two objects with identical data and children always produce the same ID — deduplication and integrity verification happen automatically, by construction, not by convention. - -Once an object is frozen, it becomes truly immutable — no updates, no deletes, enforced by triggers at the database level. New versions don't destroy old ones; they share structure with them. Change one leaf and only the nodes along the path from that leaf to the root are recreated. Everything else is reused. This is structural sharing — the same trick that makes persistent data structures in Clojure and Haskell efficient — running inside your database. +`@pgpm/object-store` implements a content-addressable Merkle tree storage system in PostgreSQL. Objects are immutable once frozen, and their IDs are deterministically derived from their content (data + children), similar to how Git stores objects. This enables efficient structural sharing, deduplication, and tamper-evident storage. ## Features -- **Content-Addressable IDs**: Object IDs are deterministically computed from content using UUID v5 hashing — same content always produces the same ID -- **Merkle Tree Structure**: Objects form a tree via parallel `kids`/`ktree` arrays, enabling structural sharing across versions -- **Immutability Enforcement**: Frozen objects cannot be modified or deleted, enforced at the trigger level -- **Path-Based Operations**: Insert, update, remove, and query nodes using hierarchical paths like a filesystem -- **Structural Sharing**: Unchanged subtrees are reused across versions (copy-on-write), keeping storage efficient -- **Recursive Traversal**: Walk entire trees or paths from root to leaf in a single query -- **Scope Isolation**: Multi-tenant by design — `scope_id` partitions all data without foreign keys -- **Array Utilities**: Built-in helper functions for array manipulation (`array_shift`, `array_pop`, `array_index_of`, etc.) +- **Content-Addressable IDs**: Object IDs are deterministically computed from content using UUID v5 hashing +- **Merkle Tree Structure**: Objects form a tree via `kids`/`ktree` arrays, enabling structural sharing +- **Immutability**: Frozen objects cannot be modified or deleted +- **Path-Based Operations**: Insert, update, remove, and query nodes using hierarchical paths +- **Structural Sharing**: Unchanged subtrees are reused across versions (copy-on-write) - **Pure plpgsql**: No external dependencies beyond pgcrypto and uuid-ossp ## Installation @@ -90,381 +81,96 @@ pgpm deploy --createdb --database mydb1 ## Core Concepts -### The Object Table - -Every object in the store is a row in `object_store_public.object`: +### Object Table -| Column | Type | Description | -|--------|------|-------------| -| `id` | uuid | Content-addressable hash, computed automatically from `data` + `kids`/`ktree` | -| `scope_id` | uuid | Tenant/namespace isolation key | -| `data` | jsonb | The object's payload — any JSON value | -| `kids` | uuid[] | Ordered array of child object IDs | -| `ktree` | text[] | Ordered array of child names (parallel to `kids`) | -| `frzn` | bool | Whether the object is frozen (immutable) | -| `created_at` | timestamptz | When the object was first stored | - -The primary key is `(id, scope_id)`. The `kids` and `ktree` arrays must always have the same length (enforced by a CHECK constraint), or both be NULL for leaf nodes. +Every object has: +- `id` (uuid) — content-addressable hash computed from data + children +- `scope_id` (uuid) — scoping identifier for multi-tenant usage +- `data` (jsonb) — the object's payload +- `kids` (uuid[]) — child object IDs +- `ktree` (text[]) — child names (parallel to `kids`) +- `frzn` (bool) — whether the object is frozen (immutable) ### Content-Addressable Hashing -Every time you insert an object, a `BEFORE INSERT` trigger computes its ID by hashing the `data` and `kids`/`ktree` arrays using UUID v5. This means: - -- Two objects with identical content always have the same ID -- Deduplication is automatic — inserting the same content twice is a no-op -- Any change to content produces a completely different ID -- You can verify integrity by recomputing the hash - -This is the same principle behind Git's object store and Merkle trees in general. - -### Structural Sharing - -When you update a single node deep in a tree, only the nodes along the path from that node to the root are recreated. All other subtrees are shared by reference: - -``` -Before: After updating C: - - R R' - / \ / \ - A B --> A B' - / \ \ / \ \ - C D E C' D E -``` - -Only R, B, and C are new objects. A, D, and E are reused. This keeps the storage cost proportional to the depth of the change, not the size of the tree. - -### Immutability and Freezing - -Objects start mutable (`frzn = false`). Once frozen via `freeze_objects()`, they become permanently immutable: - -- **Updates are blocked**: Any attempt to change a frozen object's `id`, `data`, `kids`, or `ktree` raises an exception -- **Deletes are blocked**: Frozen objects cannot be deleted -- **Freezing is recursive**: `freeze_objects()` freezes the target and all its descendants -- **The only allowed transition** is `frzn: false -> true` (you can freeze an unfrozen object, but never unfreeze) - -This gives you an append-only history where past states are preserved by construction. +Object IDs are automatically generated via a trigger that hashes the object's `data` and `kids`/`ktree` arrays. Two objects with identical content will always have the same ID, enabling natural deduplication and structural sharing. ## Core Functions -### object_store_public.insert_node_at_path(s_id, root, path, data, kids, ktree) - -Insert or replace a node at the given path, creating intermediate nodes as needed. Returns the new root ID (since all ancestors are recreated with updated children via structural sharing). +### object_store_public.insert_node_at_path(scope_id, root, path, data, kids, ktree) -**Signature:** -```sql -object_store_public.insert_node_at_path( - s_id uuid, -- scope identifier - root uuid, -- current root object ID - path text[], -- path to the target node - data jsonb, -- JSON payload for the new node - kids uuid[], -- child object IDs (empty array for leaf nodes) - ktree text[] -- child names (empty array for leaf nodes) -) RETURNS uuid -- new root ID -``` +Insert or replace a node at the given path, creating intermediate nodes as needed. Returns the new root ID (since all ancestors are recreated with updated children). -**Usage:** ```sql --- Create a root node -INSERT INTO object_store_public.object (scope_id) - VALUES ('aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa') - RETURNING id; --- Returns: - --- Insert a leaf node at path ['src', 'main.ts'] SELECT object_store_public.insert_node_at_path( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid, - ARRAY['src', 'main.ts'], - '{"content": "console.log(hello)"}'::jsonb, - ARRAY[]::uuid[], - ARRAY[]::text[] -); --- Returns: -``` - -### object_store_public.set_data_at_path(s_id, root, path, data) - -Update the data on an existing node while preserving its children. Looks up the existing node's `kids`/`ktree` first, then delegates to `insert_node_at_path`. - -**Signature:** -```sql -object_store_public.set_data_at_path( - s_id uuid, -- scope identifier - root uuid, -- current root object ID - path text[], -- path to the target node - data jsonb -- new JSON payload -) RETURNS uuid -- new root ID -``` - -**Usage:** -```sql --- Update only the data at a path, keeping children intact -SELECT object_store_public.set_data_at_path( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid, - ARRAY['src', 'main.ts'], - '{"content": "console.log(updated)"}'::jsonb + s_id := 'aaaaaaaa-...'::uuid, + root := ''::uuid, + path := ARRAY['src', 'main.ts']::text[], + data := '{"content": "console.log(hello)"}'::jsonb, + kids := ARRAY[]::uuid[], + ktree := ARRAY[]::text[] ); ``` -### object_store_public.get_node_at_path(s_id, id, path) +### object_store_public.get_node_at_path(scope_id, id, path) -Retrieve the object at a given path from a root node. Returns the full object row. +Retrieve the object at a given path from a root node. -**Signature:** ```sql -object_store_public.get_node_at_path( - s_id uuid, -- scope identifier - id uuid, -- root object ID - path text[] -- path to traverse (empty array returns the root itself) -) RETURNS object_store_public.object -``` - -**Usage:** -```sql --- Get the object at ['src', 'main.ts'] -SELECT * FROM object_store_public.get_node_at_path( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid, - ARRAY['src', 'main.ts'] -); - --- Get the root object itself SELECT * FROM object_store_public.get_node_at_path( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid, - ARRAY[]::text[] + s_id := 'aaaaaaaa-...'::uuid, + id := ''::uuid, + path := ARRAY['src', 'main.ts']::text[] ); ``` -### object_store_public.remove_node_at_path(s_id, root, path) +### object_store_public.remove_node_at_path(scope_id, root, path) -Remove a node at the given path. Returns a new root with the node removed from its parent's children. If the node doesn't exist, returns the original root unchanged. Cannot remove the root node itself. +Remove a node at the given path, returning a new root with the node removed. -**Signature:** ```sql -object_store_public.remove_node_at_path( - s_id uuid, -- scope identifier - root uuid, -- current root object ID - path text[] -- path to the node to remove -) RETURNS uuid -- new root ID -``` - -**Usage:** -```sql --- Remove the node at ['src', 'old-file.ts'] SELECT object_store_public.remove_node_at_path( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid, - ARRAY['src', 'old-file.ts'] + s_id := 'aaaaaaaa-...'::uuid, + root := ''::uuid, + path := ARRAY['src', 'old-file.ts']::text[] ); ``` -### object_store_public.get_all_objects_from_root(s_id, id) +### object_store_public.set_data_at_path(scope_id, root, path, data) -Recursively retrieve all objects in the tree starting from a root node. Uses a recursive CTE to walk the entire `kids` graph. - -**Signature:** -```sql -object_store_public.get_all_objects_from_root( - s_id uuid, -- scope identifier - id uuid -- root object ID -) RETURNS SETOF object_store_public.object -``` +Update the data on an existing node while preserving its children. -**Usage:** ```sql --- Get every object in the tree -SELECT * FROM object_store_public.get_all_objects_from_root( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid -); -``` - -### object_store_public.get_path_objects_from_root(s_id, id, path) - -Retrieve all objects along a path from root to the target node. Returns one row per node along the path, starting with the root. - -**Signature:** -```sql -object_store_public.get_path_objects_from_root( - s_id uuid, -- scope identifier - id uuid, -- root object ID - path text[] -- path to walk -) RETURNS SETOF object_store_public.object -``` - -**Usage:** -```sql --- Get every object along the path ['src', 'components', 'Button.tsx'] -SELECT * FROM object_store_public.get_path_objects_from_root( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid, - ARRAY['src', 'components', 'Button.tsx'] +SELECT object_store_public.set_data_at_path( + s_id := 'aaaaaaaa-...'::uuid, + root := ''::uuid, + path := ARRAY['src', 'main.ts']::text[], + data := '{"content": "updated content"}'::jsonb ); --- Returns: root, src node, components node, Button.tsx node ``` -### object_store_public.get_all(s_id, id) +### object_store_public.get_all_objects_from_root(scope_id, id) -Recursively retrieve all paths and their data from a root node. Returns `(path, data)` tuples for every node in the tree, walking children depth-first. +Recursively retrieve all objects in the tree starting from a root node. -**Signature:** ```sql -object_store_public.get_all( - s_id uuid, -- scope identifier - id uuid -- root object ID -) RETURNS TABLE (path text[], data jsonb) -``` - -**Usage:** -```sql --- Flatten the tree into path/data pairs -SELECT * FROM object_store_public.get_all( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid +SELECT * FROM object_store_public.get_all_objects_from_root( + s_id := 'aaaaaaaa-...'::uuid, + id := ''::uuid ); --- Returns rows like: --- path: ['src', 'main.ts'], data: '{"content": "..."}' --- path: ['src', 'utils.ts'], data: '{"content": "..."}' --- path: ['README.md'], data: '{"content": "..."}' --- path: [], data: null (root node) ``` -### object_store_public.freeze_objects(s_id, id) - -Recursively freeze an object and all its descendants. Once frozen, objects are permanently immutable — enforced by database triggers. +### object_store_public.freeze_objects(scope_id, id) -**Signature:** -```sql -object_store_public.freeze_objects( - s_id uuid, -- scope identifier - id uuid -- root object ID to freeze -) RETURNS void -``` +Freeze an object and all its descendants, making them immutable. -**Usage:** ```sql --- Freeze the entire tree SELECT object_store_public.freeze_objects( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - ''::uuid + s_id := 'aaaaaaaa-...'::uuid, + id := ''::uuid ); - --- Now any modification attempt will fail: --- UPDATE object_store_public.object SET data = '{}' WHERE id = ''; --- ERROR: you cannot mutate an immutable record. ``` -### object_store_public.update_node_at_path(s_id, root, path, data, kids, ktree) - -Replace an existing node at the given path entirely (data + children). Delegates to `insert_node_at_path`. - -**Signature:** -```sql -object_store_public.update_node_at_path( - s_id uuid, -- scope identifier - root uuid, -- current root object ID - path text[], -- path to the target node - data jsonb, -- new JSON payload - kids uuid[], -- new child object IDs - ktree text[] -- new child names -) RETURNS uuid -- new root ID -``` - -## Usage Examples - -### Building a Virtual Filesystem - -```sql -DO $$ -DECLARE - scope uuid := 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; - root_id uuid; - new_root uuid; -BEGIN - -- Create an empty root - INSERT INTO object_store_public.object (scope_id) - VALUES (scope) RETURNING id INTO root_id; - - -- Add files - SELECT object_store_public.insert_node_at_path( - scope, root_id, ARRAY['README.md'], - '{"content": "# My Project"}'::jsonb, - ARRAY[]::uuid[], ARRAY[]::text[] - ) INTO new_root; - - SELECT object_store_public.insert_node_at_path( - scope, new_root, ARRAY['src', 'index.ts'], - '{"content": "export default {}"}'::jsonb, - ARRAY[]::uuid[], ARRAY[]::text[] - ) INTO new_root; - - -- List all files - PERFORM * FROM object_store_public.get_all(scope, new_root); -END $$; -``` - -### Comparing Versions - -```sql --- After making changes, you have two root IDs (old and new). --- Get all objects from each to diff them: - -SELECT 'added' as status, path, data -FROM object_store_public.get_all(scope, new_root) -WHERE path NOT IN (SELECT path FROM object_store_public.get_all(scope, old_root)) - -UNION ALL - -SELECT 'removed' as status, path, data -FROM object_store_public.get_all(scope, old_root) -WHERE path NOT IN (SELECT path FROM object_store_public.get_all(scope, new_root)); -``` - -### Multi-Tenant Isolation - -```sql --- Each tenant gets their own scope_id. --- Objects are completely isolated between scopes. - --- Tenant A -SELECT object_store_public.insert_node_at_path( - 'aaaaaaaa-0000-0000-0000-000000000001', root_a, ... -); - --- Tenant B (completely separate namespace) -SELECT object_store_public.insert_node_at_path( - 'aaaaaaaa-0000-0000-0000-000000000002', root_b, ... -); -``` - -## Database Schema - -### Schemas - -| Schema | Purpose | -|--------|---------| -| `object_store_public` | Public API — the object table and all user-facing functions | -| `object_store_private` | Internal — hash computation and trigger functions | -| `object_store_utils` | Array utility functions used by the store internals | - -### Indexes - -| Index | Table | Columns | Purpose | -|-------|-------|---------|---------| -| `scope_id_idx` | object | `scope_id` | Fast scope-based queries | -| `frzn_idx` | object | `frzn` | Efficient frozen/unfrozen filtering | -| `object_kids_idx` | object | `kids` (GIN) | Fast child lookups for recursive traversals | - -### Triggers - -| Trigger | Event | Purpose | -|---------|-------|---------| -| `generate_id_hash` | BEFORE INSERT | Computes content-addressable ID from data + children | -| `immutable_objects` | BEFORE UPDATE | Prevents modification of frozen objects | -| `delete_immutable_objects` | BEFORE DELETE | Prevents deletion of frozen objects | - ## Testing ```bash @@ -473,21 +179,12 @@ pnpm test ## Dependencies -- [`@pgpm/verify`](https://www.npmjs.com/package/@pgpm/verify): Verification utilities - -## See Also - -- [`@pgpm/object-tree`](https://www.npmjs.com/package/@pgpm/object-tree): Git-like version control layer built on top of this package — adds commits, refs, and stores for full history tracking +- `@pgpm/verify`: Verification utilities ## Related Tooling -* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. -* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **Isolated testing environments** with per-test transaction rollbacks — ideal for integration tests, complex migrations, and RLS simulation. -* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **Supabase-native test harness** preconfigured for the local Supabase stack — per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. -* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. -* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **SQL conversion engine** that interprets and converts PostgreSQL syntax. -* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **Node.js bindings** for `libpg_query`, converting SQL into parse trees. -* [pg-proto-parser](https://github.com/constructive-io/pg-proto-parser): **Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums. +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): PostgreSQL Package Manager for modular Postgres development. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): Isolated testing environments with per-test transaction rollbacks. ## Disclaimer diff --git a/packages/object-store/package.json b/packages/object-store/package.json index b99b77dda..ea54639d7 100644 --- a/packages/object-store/package.json +++ b/packages/object-store/package.json @@ -26,7 +26,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.3" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/object-tree/README.md b/packages/object-tree/README.md index 76392c86d..ad0608d65 100644 --- a/packages/object-tree/README.md +++ b/packages/object-tree/README.md @@ -12,26 +12,22 @@

-Git-like version control for database objects, built on [`@pgpm/object-store`](https://www.npmjs.com/package/@pgpm/object-store)'s immutable Merkle trees. +Git-like version control for database objects with commits and refs ## Overview -If `@pgpm/object-store` is the storage engine, `@pgpm/object-tree` is the version control system built on top of it. Like Git, it layers commits, refs, and stores over a content-addressable object graph — but instead of tracking files on a filesystem, it tracks structured data inside PostgreSQL. - -This package was born alongside `@pgpm/object-store`, written in Egypt during the same stretch of deep work on immutable database architectures. The inspiration, again, was Rich Hickey and the ideas behind Datomic: that a database should remember everything, that time is a first-class concept, and that you should be able to ask "what did this look like yesterday?" as naturally as you ask "what does this look like now?" Where the object store provides the immutable foundation — content-addressable nodes, structural sharing, Merkle integrity — the object tree adds the narrative layer: commits that record *when* things changed, refs that name the current state, and stores that isolate independent histories from each other. - -The result is a version-controlled data layer that lives entirely inside your database. Every mutation is a commit. Every commit points to an immutable tree root. You can branch, diff, and time-travel through your data's history using the same mental model you use with Git — except the objects are rows, and the repository is a schema. +`@pgpm/object-tree` builds on top of `@pgpm/object-store` to provide a Git-like version control layer for database objects. It adds commits, refs (branches), and stores to organize content-addressable object trees into a full version history with time-travel capabilities. ## Features -- **Commits**: Track changes with parent references, messages, and timestamps — a full history chain -- **Refs / Branches**: Named pointers to commits (like Git branches), with `main` as the default -- **Stores**: Isolated repositories within a single database — multiple independent histories per scope -- **Rev-Parse**: Resolve a ref name to the current tree root in a single call -- **Set-and-Commit**: Atomic insert-and-commit in a single operation — no partial states -- **Set-Props-and-Commit**: Update node properties (preserving children) and commit atomically -- **Time Travel**: Access any historical state via commit IDs — every past tree root is still available -- **Built on @pgpm/object-store**: Inherits content-addressable hashing, structural sharing, and immutability +- **Commits**: Track changes with parent references, messages, and timestamps +- **Refs / Branches**: Named pointers to commits (like Git branches) +- **Stores**: Isolated repositories within a single database +- **Rev-Parse**: Resolve a ref name to the current tree root +- **Set-and-Commit**: Atomic insert-and-commit in a single operation +- **Set-Props-and-Commit**: Update node properties and commit atomically +- **Time Travel**: Access any historical state via commit IDs +- **Built on @pgpm/object-store**: Inherits content-addressable hashing and structural sharing ## Installation @@ -89,265 +85,88 @@ pgpm deploy --createdb --database mydb1 ### Stores -A store is an isolated object repository within a scope, analogous to a Git repository. Multiple stores can exist within the same scope for different purposes (e.g., one for metaschema definitions, another for migration state). Each store has its own independent commit history and refs. +A store is an isolated object repository within a database, analogous to a Git repository. Multiple stores can exist within the same database for different purposes (e.g., metaschema, migrations). ### Commits A commit records a snapshot of the tree at a point in time. Each commit references: - -| Field | Type | Description | -|-------|------|-------------| -| `id` | uuid | Unique commit identifier | -| `scope_id` | uuid | Tenant/namespace isolation | -| `store_id` | uuid | Which store this commit belongs to | -| `tree_id` | uuid | Root object ID of the tree at this commit | -| `parent_ids` | uuid[] | Parent commit(s) for history traversal | -| `message` | text | Descriptive commit message | -| `created_at` | timestamptz | When the commit was created | - -Because the `tree_id` points into the object store (which is immutable and content-addressable), the full state of the tree at any commit is always available — it's never overwritten or garbage-collected. +- `tree_id` — the root object of the tree at this commit +- `parent_ids` — parent commit(s) for history traversal +- `store_id` — which store this commit belongs to +- `message` — descriptive commit message ### Refs -A ref is a named pointer to a commit, exactly like a Git branch. The default ref is `main`. When you make a new commit, the ref is updated to point to the new commit — but the old commit (and its tree) remain accessible by ID. - -| Field | Type | Description | -|-------|------|-------------| -| `id` | uuid | Unique ref identifier | -| `scope_id` | uuid | Tenant/namespace isolation | -| `store_id` | uuid | Which store this ref belongs to | -| `name` | text | The ref name (e.g., `main`) | -| `commit_id` | uuid | The commit this ref currently points to | - -### Scope and Store: Two Levels of Isolation - -- **`scope_id`** isolates tenants — it's on every table and prevents cross-tenant access entirely -- **`store_id`** isolates repositories *within* a tenant — different stores have independent commit histories, refs, and trees, but objects in the underlying store can be shared across stores via structural sharing +A ref is a named pointer to a commit (like a Git branch). The default ref is `main`. ## Core Functions -### object_tree_public.init_empty_repo(s_id, store_id) - -Initialize a new repository with an empty root object, a `main` ref, and a first commit. Raises `REPO_EXISTS` if the store already has commits. +### object_tree_public.init_empty_repo(scope_id, store_id) -**Signature:** -```sql -object_tree_public.init_empty_repo( - s_id uuid, -- scope identifier - store_id uuid -- store identifier for the new repo -) RETURNS void -``` +Initialize an empty repository with a root object, initial commit, and `main` ref. -**Usage:** ```sql SELECT object_tree_public.init_empty_repo( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb' + s_id := 'aaaaaaaa-...'::uuid, + store_id := 'bbbbbbbb-...'::uuid ); --- Creates: empty root object, 'main' ref, and first commit ``` -### object_tree_public.set_and_commit(s_id, store_id, refname, path, data, kids, ktree) +### object_tree_public.set_and_commit(scope_id, store_id, refname, path, data, kids, ktree) -Insert or replace a node at the given path and create a new commit in one atomic operation. Resolves the current ref, inserts the node into the current tree (via `object_store_public.insert_node_at_path`), creates a commit with the new tree root, and advances the ref. +Insert a node at the given path and create a new commit atomically. -**Signature:** ```sql -object_tree_public.set_and_commit( - s_id uuid, -- scope identifier - store_id uuid, -- store identifier - refname text, -- ref to commit on (e.g., 'main') - path text[], -- path to the target node - data jsonb, -- JSON payload for the new node - kids uuid[], -- child object IDs - ktree text[] -- child names -) RETURNS uuid -- new tree root ID -``` - -**Usage:** -```sql --- Add a file and commit in one step SELECT object_tree_public.set_and_commit( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', - 'main', - ARRAY['src', 'main.ts'], - '{"content": "hello world"}'::jsonb, - ARRAY[]::uuid[], - ARRAY[]::text[] + s_id := 'aaaaaaaa-...'::uuid, + store_id := 'bbbbbbbb-...'::uuid, + refname := 'main', + path := ARRAY['src', 'main.ts']::text[], + data := '{"content": "hello world"}'::jsonb, + kids := ARRAY[]::uuid[], + ktree := ARRAY[]::text[] ); ``` -### object_tree_public.set_props_and_commit(s_id, store_id, refname, path, data) - -Update the data on an existing node (preserving its children) and commit atomically. Uses `object_store_public.set_data_at_path` under the hood. +### object_tree_public.set_props_and_commit(scope_id, store_id, refname, path, data) -**Signature:** -```sql -object_tree_public.set_props_and_commit( - s_id uuid, -- scope identifier - store_id uuid, -- store identifier - refname text, -- ref to commit on (e.g., 'main') - path text[], -- path to the target node - data jsonb -- new JSON payload (children preserved) -) RETURNS uuid -- new tree root ID -``` +Update the properties of an existing node (preserving children) and commit. -**Usage:** ```sql --- Update properties on an existing node and commit SELECT object_tree_public.set_props_and_commit( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', - 'main', - ARRAY['src', 'main.ts'], - '{"content": "updated content"}'::jsonb + s_id := 'aaaaaaaa-...'::uuid, + store_id := 'bbbbbbbb-...'::uuid, + refname := 'main', + path := ARRAY['src', 'main.ts']::text[], + data := '{"content": "updated"}'::jsonb ); ``` -### object_tree_public.rev_parse(s_id, store_id, refname) +### object_tree_public.rev_parse(scope_id, store_id, refname) -Resolve a ref name to its current tree root ID. Follows the ref to its commit, then returns the commit's `tree_id`. The `refname` parameter defaults to `'main'`. +Resolve a ref name to its current tree root ID. -**Signature:** ```sql -object_tree_public.rev_parse( - s_id uuid, -- scope identifier - store_id uuid, -- store identifier - refname text -- ref name to resolve (default: 'main') -) RETURNS uuid -- tree root ID -``` - -**Usage:** -```sql --- Get the current tree root for 'main' SELECT object_tree_public.rev_parse( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', - 'main' + s_id := 'aaaaaaaa-...'::uuid, + store_id := 'bbbbbbbb-...'::uuid, + refname := 'main' ); ``` -### object_tree_public.get_object_at_path(s_id, store_id, path, refname) +### object_tree_public.get_object_at_path(scope_id, store_id, path, refname) -Get the object at a path for a given ref. Combines `rev_parse` and `object_store_public.get_node_at_path` into a single call. The `refname` parameter defaults to `'main'`. +Get the object at a path for a given ref. -**Signature:** ```sql -object_tree_public.get_object_at_path( - s_id uuid, -- scope identifier - store_id uuid, -- store identifier - path text[], -- path to the target node - refname text -- ref name (default: 'main') -) RETURNS object_store_public.object -``` - -**Usage:** -```sql --- Get the object at a path on the main branch SELECT * FROM object_tree_public.get_object_at_path( - 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa', - 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb', - ARRAY['src', 'main.ts'], - 'main' -); -``` - -## Usage Examples - -### Setting Up a Repository and Making Changes - -```sql -DO $$ -DECLARE - scope uuid := 'aaaaaaaa-aaaa-aaaa-aaaa-aaaaaaaaaaaa'; - store uuid := 'bbbbbbbb-bbbb-bbbb-bbbb-bbbbbbbbbbbb'; - tree_root uuid; -BEGIN - -- Initialize a new repo - PERFORM object_tree_public.init_empty_repo(scope, store); - - -- Add a file and commit - SELECT object_tree_public.set_and_commit( - scope, store, 'main', - ARRAY['README.md'], - '{"content": "# My Project"}'::jsonb, - ARRAY[]::uuid[], ARRAY[]::text[] - ) INTO tree_root; - - -- Add another file and commit - SELECT object_tree_public.set_and_commit( - scope, store, 'main', - ARRAY['src', 'index.ts'], - '{"content": "export default {}"}'::jsonb, - ARRAY[]::uuid[], ARRAY[]::text[] - ) INTO tree_root; - - -- Update a file's content (preserving children) and commit - SELECT object_tree_public.set_props_and_commit( - scope, store, 'main', - ARRAY['README.md'], - '{"content": "# My Project\n\nUpdated readme."}'::jsonb - ) INTO tree_root; -END $$; -``` - -### Time Travel: Reading Historical State - -```sql --- Every commit stores its tree_id, so you can read any past state: - --- Get all commits for a store (newest first) -SELECT id, tree_id, message, created_at -FROM object_tree_public.commit -WHERE scope_id = 'aaaaaaaa-...' AND store_id = 'bbbbbbbb-...' -ORDER BY created_at DESC; - --- Read the tree at any historical commit -SELECT * FROM object_store_public.get_all( - 'aaaaaaaa-...', - 'historical-tree-id-from-commit'::uuid + s_id := 'aaaaaaaa-...'::uuid, + store_id := 'bbbbbbbb-...'::uuid, + path := ARRAY['src', 'main.ts']::text[], + refname := 'main' ); ``` -### Multiple Stores in One Scope - -```sql --- Store 1: metaschema definitions -SELECT object_tree_public.init_empty_repo(scope, store_metaschema); -SELECT object_tree_public.set_and_commit( - scope, store_metaschema, 'main', - ARRAY['tables', 'users'], - '{"columns": ["id", "email", "name"]}'::jsonb, - ARRAY[]::uuid[], ARRAY[]::text[] -); - --- Store 2: migration state (completely independent history) -SELECT object_tree_public.init_empty_repo(scope, store_migrations); -SELECT object_tree_public.set_and_commit( - scope, store_migrations, 'main', - ARRAY['001_init'], - '{"applied": true, "at": "2026-01-15"}'::jsonb, - ARRAY[]::uuid[], ARRAY[]::text[] -); -``` - -## Database Schema - -### Tables - -| Table | Schema | Purpose | -|-------|--------|---------| -| `commit` | `object_tree_public` | Commit history with tree snapshots and parent links | -| `ref` | `object_tree_public` | Named pointers (branches) to commits | -| `store` | `object_tree_public` | Store registry (optional metadata for stores) | - -### Schemas - -| Schema | Purpose | -|--------|---------| -| `object_tree_public` | Public API — commits, refs, stores, and all user-facing functions | - ## Testing ```bash @@ -356,18 +175,13 @@ pnpm test ## Dependencies -- [`@pgpm/object-store`](https://www.npmjs.com/package/@pgpm/object-store): Content-addressable Merkle tree storage — the immutable foundation this package builds on -- [`@pgpm/verify`](https://www.npmjs.com/package/@pgpm/verify): Verification utilities +- `@pgpm/object-store`: Content-addressable Merkle tree storage +- `@pgpm/verify`: Verification utilities ## Related Tooling -* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. -* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **Isolated testing environments** with per-test transaction rollbacks — ideal for integration tests, complex migrations, and RLS simulation. -* [supabase-test](https://github.com/constructive-io/constructive/tree/main/packages/supabase-test): **Supabase-native test harness** preconfigured for the local Supabase stack — per-test rollbacks, JWT/role context helpers, and CI/GitHub Actions ready. -* [graphile-test](https://github.com/constructive-io/constructive/tree/main/packages/graphile-test): **Authentication mocking** for Graphile-focused test helpers and emulating row-level security contexts. -* [pgsql-parser](https://github.com/constructive-io/pgsql-parser): **SQL conversion engine** that interprets and converts PostgreSQL syntax. -* [libpg-query-node](https://github.com/constructive-io/libpg-query-node): **Node.js bindings** for `libpg_query`, converting SQL into parse trees. -* [pg-proto-parser](https://github.com/constructive-io/pg-proto-parser): **Protobuf parser** for parsing PostgreSQL Protocol Buffers definitions to generate TypeScript interfaces, utility functions, and JSON mappings for enums. +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): PostgreSQL Package Manager for modular Postgres development. +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): Isolated testing environments with per-test transaction rollbacks. ## Disclaimer diff --git a/packages/object-tree/package.json b/packages/object-tree/package.json index 22151ce21..8e98b5327 100644 --- a/packages/object-tree/package.json +++ b/packages/object-tree/package.json @@ -28,7 +28,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.3" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql index c3f1724b2..f8447ee3a 100644 --- a/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql +++ b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql @@ -2,7 +2,6 @@ BEGIN; -DROP FUNCTION object_tree_public.set_props_and_commit; DROP FUNCTION object_tree_public.set_and_commit; COMMIT; diff --git a/packages/partman/README.md b/packages/partman/README.md index 2609c6dc8..c22ea2f14 100644 --- a/packages/partman/README.md +++ b/packages/partman/README.md @@ -12,7 +12,7 @@

-pg_partman wrapper for pgpm — installs `pg_partman` into the `partman` schema. +pg_partman wrapper for pgpm — installs `pg_partman` into the `partman` schema ## Overview @@ -125,9 +125,9 @@ pnpm test ## Related Tooling -* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): **PostgreSQL Package Manager** for modular Postgres development. Works with database workspaces, scaffolding, migrations, seeding, and installing database packages. -* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): **Isolated testing environments** with per-test transaction rollbacks — ideal for integration tests, complex migrations, and RLS simulation. -* [pg_partman](https://github.com/pgpartman/pg_partman): **PostgreSQL Partition Manager** for automated partition creation and maintenance. +* [pgpm](https://github.com/constructive-io/constructive/tree/main/packages/pgpm): PostgreSQL Package Manager for modular Postgres development +* [pgsql-test](https://github.com/constructive-io/constructive/tree/main/packages/pgsql-test): Isolated testing environments with per-test transaction rollbacks +* [pg_partman](https://github.com/pgpartman/pg_partman): PostgreSQL Partition Manager ## Disclaimer diff --git a/packages/partman/deploy/schemas/partman/procedures/create_parent_by_id.sql b/packages/partman/deploy/schemas/partman/procedures/create_parent_by_id.sql index 4c99d4b70..fe1f08a64 100644 --- a/packages/partman/deploy/schemas/partman/procedures/create_parent_by_id.sql +++ b/packages/partman/deploy/schemas/partman/procedures/create_parent_by_id.sql @@ -1,6 +1,7 @@ -- Deploy schemas/partman/procedures/create_parent_by_id to pg -- requires: extensions/pg_partman +-- requires: schemas/partman/procedures/create_parent_with_retention BEGIN; @@ -9,7 +10,9 @@ CREATE FUNCTION partman.create_parent_by_id( v_control text, v_type text DEFAULT 'range', partition_interval text DEFAULT '1 day', - v_premake int DEFAULT 2 + v_premake int DEFAULT 2, + v_retention text DEFAULT NULL, + v_retention_keep_table boolean DEFAULT true ) RETURNS void AS $$ DECLARE v_parent_table text; @@ -24,12 +27,14 @@ BEGIN RAISE EXCEPTION 'partman.create_parent_by_id: table_id % not found', v_table_id; END IF; - PERFORM partman.create_parent( - p_parent_table := v_parent_table, - p_control := v_control, - p_type := v_type, - p_interval := partition_interval, - p_premake := v_premake + PERFORM partman.create_parent_with_retention( + v_parent_table := v_parent_table, + v_control := v_control, + v_type := v_type, + partition_interval := partition_interval, + v_premake := v_premake, + v_retention := v_retention, + v_retention_keep_table := v_retention_keep_table ); END; $$ diff --git a/packages/partman/deploy/schemas/partman/procedures/create_parent_with_retention.sql b/packages/partman/deploy/schemas/partman/procedures/create_parent_with_retention.sql new file mode 100644 index 000000000..c8da2da30 --- /dev/null +++ b/packages/partman/deploy/schemas/partman/procedures/create_parent_with_retention.sql @@ -0,0 +1,35 @@ +-- Deploy schemas/partman/procedures/create_parent_with_retention to pg + +-- requires: extensions/pg_partman + +BEGIN; + +CREATE FUNCTION partman.create_parent_with_retention( + v_parent_table text, + v_control text, + v_type text DEFAULT 'range', + partition_interval text DEFAULT '1 day', + v_premake int DEFAULT 2, + v_retention text DEFAULT NULL, + v_retention_keep_table boolean DEFAULT true +) RETURNS void AS $$ +BEGIN + PERFORM partman.create_parent( + p_parent_table := v_parent_table, + p_control := v_control, + p_type := v_type, + p_interval := partition_interval, + p_premake := v_premake + ); + + IF v_retention IS NOT NULL THEN + UPDATE partman.part_config + SET retention = v_retention, + retention_keep_table = v_retention_keep_table + WHERE parent_table = v_parent_table; + END IF; +END; +$$ +LANGUAGE 'plpgsql' VOLATILE; + +COMMIT; diff --git a/packages/partman/package.json b/packages/partman/package.json index 4e2828ebf..54d410238 100644 --- a/packages/partman/package.json +++ b/packages/partman/package.json @@ -22,7 +22,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.24.3" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/partman/pgpm.plan b/packages/partman/pgpm.plan index 36e72c11d..04cc71c39 100644 --- a/packages/partman/pgpm.plan +++ b/packages/partman/pgpm.plan @@ -3,7 +3,8 @@ %uri=pgpm-partman extensions/pg_partman 2026-05-09T00:00:00Z devin # add pg_partman extension in partman schema -schemas/partman/procedures/create_parent_by_id [extensions/pg_partman] 2026-05-16T00:00:00Z devin # metaschema-aware wrapper for partman.create_parent +schemas/partman/procedures/create_parent_with_retention [extensions/pg_partman] 2026-05-18T00:00:00Z devin # raw-name wrapper: create_parent + retention config +schemas/partman/procedures/create_parent_by_id [extensions/pg_partman schemas/partman/procedures/create_parent_with_retention] 2026-05-16T00:00:00Z devin # metaschema-aware wrapper for partman.create_parent schemas/partman/procedures/remove_parent_by_id [extensions/pg_partman] 2026-05-16T00:00:01Z devin # metaschema-aware wrapper for partman remove schemas/partman/procedures/verify_parent_by_id [extensions/pg_partman] 2026-05-16T00:00:02Z devin # metaschema-aware wrapper for partman verify schemas/partman/procedures/run_maintenance_by_id [extensions/pg_partman] 2026-05-16T00:00:03Z devin # metaschema-aware wrapper for partman run_maintenance diff --git a/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql b/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql index 70ca1a81f..fd2f9d580 100644 --- a/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql +++ b/packages/partman/revert/schemas/partman/procedures/create_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -DROP FUNCTION IF EXISTS partman.create_parent_by_id(uuid, text, text, text, int); +DROP FUNCTION IF EXISTS partman.create_parent_by_id(uuid, text, text, text, int, text, boolean); COMMIT; diff --git a/packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql b/packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql new file mode 100644 index 000000000..ee322e788 --- /dev/null +++ b/packages/partman/revert/schemas/partman/procedures/create_parent_with_retention.sql @@ -0,0 +1,7 @@ +-- Revert schemas/partman/procedures/create_parent_with_retention from pg + +BEGIN; + +DROP FUNCTION IF EXISTS partman.create_parent_with_retention(text, text, text, text, int, text, boolean); + +COMMIT; diff --git a/packages/partman/sql/pgpm-partman--0.0.1.sql b/packages/partman/sql/pgpm-partman--0.0.1.sql index 7b4defa3a..be912cb23 100644 --- a/packages/partman/sql/pgpm-partman--0.0.1.sql +++ b/packages/partman/sql/pgpm-partman--0.0.1.sql @@ -6,13 +6,26 @@ BEGIN END; $EOFCODE$; -CREATE FUNCTION partman.create_parent_by_id( - v_table_id uuid, - v_control text, - v_type text DEFAULT 'range', - partition_interval text DEFAULT '1 day', - v_premake int DEFAULT 2 -) RETURNS void AS $EOFCODE$ +CREATE FUNCTION partman.create_parent_with_retention(v_parent_table text, v_control text, v_type text DEFAULT 'range', partition_interval text DEFAULT '1 day', v_premake int DEFAULT 2, v_retention text DEFAULT NULL, v_retention_keep_table boolean DEFAULT true) RETURNS void AS $EOFCODE$ +BEGIN + PERFORM partman.create_parent( + p_parent_table := v_parent_table, + p_control := v_control, + p_type := v_type, + p_interval := partition_interval, + p_premake := v_premake + ); + + IF v_retention IS NOT NULL THEN + UPDATE partman.part_config + SET retention = v_retention, + retention_keep_table = v_retention_keep_table + WHERE parent_table = v_parent_table; + END IF; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE FUNCTION partman.create_parent_by_id(v_table_id uuid, v_control text, v_type text DEFAULT 'range', partition_interval text DEFAULT '1 day', v_premake int DEFAULT 2, v_retention text DEFAULT NULL, v_retention_keep_table boolean DEFAULT true) RETURNS void AS $EOFCODE$ DECLARE v_parent_table text; BEGIN @@ -26,19 +39,19 @@ BEGIN RAISE EXCEPTION 'partman.create_parent_by_id: table_id % not found', v_table_id; END IF; - PERFORM partman.create_parent( - p_parent_table := v_parent_table, - p_control := v_control, - p_type := v_type, - p_interval := partition_interval, - p_premake := v_premake + PERFORM partman.create_parent_with_retention( + v_parent_table := v_parent_table, + v_control := v_control, + v_type := v_type, + partition_interval := partition_interval, + v_premake := v_premake, + v_retention := v_retention, + v_retention_keep_table := v_retention_keep_table ); END; $EOFCODE$ LANGUAGE plpgsql VOLATILE; -CREATE FUNCTION partman.remove_parent_by_id( - v_table_id uuid -) RETURNS void AS $EOFCODE$ +CREATE FUNCTION partman.remove_parent_by_id(v_table_id uuid) RETURNS void AS $EOFCODE$ DECLARE v_parent_table text; BEGIN @@ -57,9 +70,7 @@ BEGIN END; $EOFCODE$ LANGUAGE plpgsql VOLATILE; -CREATE FUNCTION partman.verify_parent_by_id( - v_table_id uuid -) RETURNS boolean AS $EOFCODE$ +CREATE FUNCTION partman.verify_parent_by_id(v_table_id uuid) RETURNS boolean AS $EOFCODE$ DECLARE v_parent_table text; v_found boolean; @@ -83,10 +94,7 @@ BEGIN END; $EOFCODE$ LANGUAGE plpgsql STABLE; -CREATE FUNCTION partman.run_maintenance_by_id( - v_table_id uuid DEFAULT NULL, - v_analyze boolean DEFAULT true -) RETURNS void AS $EOFCODE$ +CREATE FUNCTION partman.run_maintenance_by_id(v_table_id uuid DEFAULT NULL, v_analyze boolean DEFAULT true) RETURNS void AS $EOFCODE$ DECLARE v_parent_table text; BEGIN diff --git a/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql b/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql index b8ced708a..6a605d104 100644 --- a/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql +++ b/packages/partman/verify/schemas/partman/procedures/create_parent_by_id.sql @@ -2,6 +2,6 @@ BEGIN; -SELECT has_function_privilege('partman.create_parent_by_id(uuid, text, text, text, int)', 'execute'); +SELECT has_function_privilege('partman.create_parent_by_id(uuid, text, text, text, int, text, boolean)', 'execute'); ROLLBACK; diff --git a/packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql b/packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql new file mode 100644 index 000000000..e16d4fda4 --- /dev/null +++ b/packages/partman/verify/schemas/partman/procedures/create_parent_with_retention.sql @@ -0,0 +1,7 @@ +-- Verify schemas/partman/procedures/create_parent_with_retention on pg + +BEGIN; + +SELECT has_function_privilege('partman.create_parent_with_retention(text, text, text, text, int, text, boolean)', 'execute'); + +ROLLBACK; diff --git a/packages/services/deploy/schemas/services_public/tables/api_settings/table.sql b/packages/services/deploy/schemas/services_public/tables/api_settings/table.sql index 40d1806dd..58dbe5e7c 100644 --- a/packages/services/deploy/schemas/services_public/tables/api_settings/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/api_settings/table.sql @@ -23,6 +23,7 @@ CREATE TABLE services_public.api_settings ( enable_ltree boolean, enable_llm boolean, enable_realtime boolean, + enable_bulk boolean, -- Extensible JSON for future settings that don't warrant their own column options jsonb NOT NULL DEFAULT '{}'::jsonb, @@ -47,6 +48,7 @@ COMMENT ON COLUMN services_public.api_settings.enable_connection_filter IS 'Over COMMENT ON COLUMN services_public.api_settings.enable_ltree IS 'Override: enable ltree hierarchical data type (NULL = inherit from database_settings)'; COMMENT ON COLUMN services_public.api_settings.enable_llm IS 'Override: enable LLM/AI integration features (NULL = inherit from database_settings)'; COMMENT ON COLUMN services_public.api_settings.enable_realtime IS 'Override: enable realtime subscriptions (NULL = inherit from database_settings)'; +COMMENT ON COLUMN services_public.api_settings.enable_bulk IS 'Override: enable bulk mutations (NULL = inherit from database_settings)'; COMMENT ON COLUMN services_public.api_settings.options IS 'Extensible JSON for additional per-API settings that do not have dedicated columns'; CREATE INDEX api_settings_database_id_idx ON services_public.api_settings (database_id); diff --git a/packages/services/deploy/schemas/services_public/tables/database_settings/table.sql b/packages/services/deploy/schemas/services_public/tables/database_settings/table.sql index 09b917182..a8ab600c6 100644 --- a/packages/services/deploy/schemas/services_public/tables/database_settings/table.sql +++ b/packages/services/deploy/schemas/services_public/tables/database_settings/table.sql @@ -20,6 +20,7 @@ CREATE TABLE services_public.database_settings ( enable_ltree boolean NOT NULL DEFAULT true, enable_llm boolean NOT NULL DEFAULT false, enable_realtime boolean NOT NULL DEFAULT false, + enable_bulk boolean NOT NULL DEFAULT false, -- Extensible JSON for future settings that don't warrant their own column options jsonb NOT NULL DEFAULT '{}'::jsonb, @@ -42,6 +43,7 @@ COMMENT ON COLUMN services_public.database_settings.enable_connection_filter IS COMMENT ON COLUMN services_public.database_settings.enable_ltree IS 'Enable ltree hierarchical data type support in the GraphQL API'; COMMENT ON COLUMN services_public.database_settings.enable_llm IS 'Enable LLM/AI integration features in the GraphQL API'; COMMENT ON COLUMN services_public.database_settings.enable_realtime IS 'Enable realtime subscriptions (cursor-tracked change delivery) in the GraphQL API'; +COMMENT ON COLUMN services_public.database_settings.enable_bulk IS 'Enable bulk mutation operations (insert, upsert, update, delete) in the GraphQL API'; COMMENT ON COLUMN services_public.database_settings.options IS 'Extensible JSON for additional settings that do not have dedicated columns'; CREATE INDEX database_settings_database_id_idx ON services_public.database_settings (database_id); diff --git a/packages/services/package.json b/packages/services/package.json index ee6028d28..402ea069f 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/services/verify/schemas/services_public/tables/api_settings/table.sql b/packages/services/verify/schemas/services_public/tables/api_settings/table.sql index 0f5a34107..b5f46e8e8 100644 --- a/packages/services/verify/schemas/services_public/tables/api_settings/table.sql +++ b/packages/services/verify/schemas/services_public/tables/api_settings/table.sql @@ -16,6 +16,7 @@ SELECT enable_ltree, enable_llm, enable_realtime, + enable_bulk, options FROM services_public.api_settings WHERE false; diff --git a/packages/services/verify/schemas/services_public/tables/database_settings/table.sql b/packages/services/verify/schemas/services_public/tables/database_settings/table.sql index 73ef095ed..64c15c828 100644 --- a/packages/services/verify/schemas/services_public/tables/database_settings/table.sql +++ b/packages/services/verify/schemas/services_public/tables/database_settings/table.sql @@ -15,6 +15,7 @@ SELECT enable_ltree, enable_llm, enable_realtime, + enable_bulk, options FROM services_public.database_settings WHERE false; diff --git a/packages/stamps/jest.config.js b/packages/stamps/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/stamps/jest.config.js +++ b/packages/stamps/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/stamps/package.json b/packages/stamps/package.json index 7884a2eda..8f77412db 100644 --- a/packages/stamps/package.json +++ b/packages/stamps/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/totp/jest.config.js b/packages/totp/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/totp/jest.config.js +++ b/packages/totp/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/totp/package.json b/packages/totp/package.json index 0059b3497..a269906a0 100644 --- a/packages/totp/package.json +++ b/packages/totp/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/types/jest.config.js b/packages/types/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/types/jest.config.js +++ b/packages/types/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/types/package.json b/packages/types/package.json index 372f19d1c..5b96bff67 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/utils/jest.config.js b/packages/utils/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/utils/jest.config.js +++ b/packages/utils/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/utils/package.json b/packages/utils/package.json index 7643e10fa..58c94dc81 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", diff --git a/packages/verify/jest.config.js b/packages/verify/jest.config.js index e20e7efb5..4108cf46f 100644 --- a/packages/verify/jest.config.js +++ b/packages/verify/jest.config.js @@ -1,5 +1,6 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { + forceExit: true, preset: 'ts-jest', testEnvironment: 'node', diff --git a/packages/verify/package.json b/packages/verify/package.json index c1831dd00..a82dbc077 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.23.2" + "pgpm": "^4.24.4" }, "repository": { "type": "git", From 60b26ef82be1c2dac6bf0a1d6fd7bbe1d03ad66e Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 19 May 2026 21:09:24 +0000 Subject: [PATCH 2/4] fix: revert pgpm dep bump to preserve lockfile compatibility --- packages/achievements/package.json | 2 +- packages/base32/package.json | 2 +- packages/database-jobs/package.json | 2 +- packages/defaults/package.json | 2 +- packages/encrypted-secrets-table/package.json | 2 +- packages/encrypted-secrets/package.json | 2 +- packages/faker/package.json | 2 +- packages/geotypes/package.json | 2 +- packages/inflection/package.json | 2 +- packages/jobs/package.json | 2 +- packages/jwt-claims/package.json | 2 +- packages/ltree-helpers/package.json | 2 +- packages/measurements/package.json | 2 +- packages/metaschema-modules/package.json | 2 +- packages/metaschema-schema/package.json | 2 +- packages/object-store/package.json | 2 +- packages/object-tree/package.json | 2 +- packages/partman/package.json | 2 +- packages/services/package.json | 2 +- packages/stamps/package.json | 2 +- packages/totp/package.json | 2 +- packages/types/package.json | 2 +- packages/utils/package.json | 2 +- packages/verify/package.json | 2 +- 24 files changed, 24 insertions(+), 24 deletions(-) diff --git a/packages/achievements/package.json b/packages/achievements/package.json index 9abf88b43..b48cf2444 100644 --- a/packages/achievements/package.json +++ b/packages/achievements/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/base32/package.json b/packages/base32/package.json index 40803b771..7c2bf24ae 100644 --- a/packages/base32/package.json +++ b/packages/base32/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/database-jobs/package.json b/packages/database-jobs/package.json index 6f0ca66ec..819fb316e 100644 --- a/packages/database-jobs/package.json +++ b/packages/database-jobs/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "dependencies": { "@pgpm/jwt-claims": "workspace:*", diff --git a/packages/defaults/package.json b/packages/defaults/package.json index 4fd14d861..92d4602af 100644 --- a/packages/defaults/package.json +++ b/packages/defaults/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/encrypted-secrets-table/package.json b/packages/encrypted-secrets-table/package.json index 76e76883e..51d4d338b 100644 --- a/packages/encrypted-secrets-table/package.json +++ b/packages/encrypted-secrets-table/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/encrypted-secrets/package.json b/packages/encrypted-secrets/package.json index b9ab932ef..d8f65d338 100644 --- a/packages/encrypted-secrets/package.json +++ b/packages/encrypted-secrets/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/faker/package.json b/packages/faker/package.json index 28d7e1805..a7a7f7afd 100644 --- a/packages/faker/package.json +++ b/packages/faker/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/geotypes/package.json b/packages/geotypes/package.json index 7e719d498..3a7360768 100644 --- a/packages/geotypes/package.json +++ b/packages/geotypes/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/inflection/package.json b/packages/inflection/package.json index 06e03b7ff..3190daac2 100644 --- a/packages/inflection/package.json +++ b/packages/inflection/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/jobs/package.json b/packages/jobs/package.json index f8443d85a..18a584891 100644 --- a/packages/jobs/package.json +++ b/packages/jobs/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/jwt-claims/package.json b/packages/jwt-claims/package.json index 813d2f158..604d1b1c2 100644 --- a/packages/jwt-claims/package.json +++ b/packages/jwt-claims/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "dependencies": { "@pgpm/types": "workspace:*", diff --git a/packages/ltree-helpers/package.json b/packages/ltree-helpers/package.json index 78009f4a5..ac5e74376 100644 --- a/packages/ltree-helpers/package.json +++ b/packages/ltree-helpers/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/measurements/package.json b/packages/measurements/package.json index a55ee515d..a3b7e0cb4 100644 --- a/packages/measurements/package.json +++ b/packages/measurements/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/metaschema-modules/package.json b/packages/metaschema-modules/package.json index 250ff34ba..9a1c00b84 100644 --- a/packages/metaschema-modules/package.json +++ b/packages/metaschema-modules/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/metaschema-schema/package.json b/packages/metaschema-schema/package.json index 80bbb9b11..582e61f59 100644 --- a/packages/metaschema-schema/package.json +++ b/packages/metaschema-schema/package.json @@ -27,7 +27,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/object-store/package.json b/packages/object-store/package.json index ea54639d7..b99b77dda 100644 --- a/packages/object-store/package.json +++ b/packages/object-store/package.json @@ -26,7 +26,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.24.3" }, "repository": { "type": "git", diff --git a/packages/object-tree/package.json b/packages/object-tree/package.json index 8e98b5327..22151ce21 100644 --- a/packages/object-tree/package.json +++ b/packages/object-tree/package.json @@ -28,7 +28,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.24.3" }, "repository": { "type": "git", diff --git a/packages/partman/package.json b/packages/partman/package.json index 54d410238..4e2828ebf 100644 --- a/packages/partman/package.json +++ b/packages/partman/package.json @@ -22,7 +22,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.24.3" }, "repository": { "type": "git", diff --git a/packages/services/package.json b/packages/services/package.json index 402ea069f..ee6028d28 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/stamps/package.json b/packages/stamps/package.json index 8f77412db..7884a2eda 100644 --- a/packages/stamps/package.json +++ b/packages/stamps/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/totp/package.json b/packages/totp/package.json index a269906a0..0059b3497 100644 --- a/packages/totp/package.json +++ b/packages/totp/package.json @@ -25,7 +25,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/types/package.json b/packages/types/package.json index 5b96bff67..372f19d1c 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/utils/package.json b/packages/utils/package.json index 58c94dc81..7643e10fa 100644 --- a/packages/utils/package.json +++ b/packages/utils/package.json @@ -24,7 +24,7 @@ "@pgpm/verify": "workspace:*" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", diff --git a/packages/verify/package.json b/packages/verify/package.json index a82dbc077..c1831dd00 100644 --- a/packages/verify/package.json +++ b/packages/verify/package.json @@ -21,7 +21,7 @@ "test:watch": "jest --watch" }, "devDependencies": { - "pgpm": "^4.24.4" + "pgpm": "^4.23.2" }, "repository": { "type": "git", From 38fed1116d72e639fdcfab3ee8db6610c03d3323 Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 19 May 2026 21:11:27 +0000 Subject: [PATCH 3/4] fix: restore set_props_and_commit DROP in object-tree revert The deploy creates both set_and_commit and set_props_and_commit, so the revert must drop both to allow the schema to be dropped cleanly. --- .../schemas/object_tree_public/procedures/set_and_commit.sql | 1 + 1 file changed, 1 insertion(+) diff --git a/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql index f8447ee3a..c3f1724b2 100644 --- a/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql +++ b/packages/object-tree/revert/schemas/object_tree_public/procedures/set_and_commit.sql @@ -2,6 +2,7 @@ BEGIN; +DROP FUNCTION object_tree_public.set_props_and_commit; DROP FUNCTION object_tree_public.set_and_commit; COMMIT; From f2e7d08ee91b3cc1b76796264a414960f77ac61d Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Tue, 19 May 2026 21:16:27 +0000 Subject: [PATCH 4/4] fix: revert stale verify files, database-jobs tests; update module snapshots - Revert verify files for blueprint, relation_provision, secure_table_provision (constructive-db verifies referenced columns that don't exist in deploy) - Revert database-jobs test changes (function signature mismatch) - Update metaschema-modules test snapshots for new module table names --- .../__tests__/__snapshots__/jobs.test.ts.snap | 1 + packages/database-jobs/__tests__/jobs.test.ts | 35 +++++++++---------- .../__snapshots__/modules.test.ts.snap | 17 +++++---- .../tables/blueprint/table.sql | 5 --- .../tables/relation_provision/table.sql | 5 +-- .../tables/secure_table_provision/table.sql | 3 +- 6 files changed, 32 insertions(+), 34 deletions(-) diff --git a/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap b/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap index bda5397b0..2a9114b79 100644 --- a/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap +++ b/packages/database-jobs/__tests__/__snapshots__/jobs.test.ts.snap @@ -2,6 +2,7 @@ exports[`scheduled jobs schedule jobs 1`] = ` { + "actor_id": null, "attempts": 0, "database_id": "5b720132-17d5-424d-9bcb-ee7b17c13d43", "id": "1", diff --git a/packages/database-jobs/__tests__/jobs.test.ts b/packages/database-jobs/__tests__/jobs.test.ts index 453de4c04..44297918a 100644 --- a/packages/database-jobs/__tests__/jobs.test.ts +++ b/packages/database-jobs/__tests__/jobs.test.ts @@ -65,19 +65,20 @@ describe('scheduled jobs', () => { const start = new Date(Date.now() + 10000); // 10s const end = new Date(start.getTime() + 180000); // +3min + // Set JWT claims for the session (required by add_scheduled_job) + await pg.any(`SELECT set_config('jwt.claims.database_id', $1, false)`, [database_id]); + const [result] = await pg.any( `SELECT * FROM app_jobs.add_scheduled_job( - db_id := $1::uuid, - identifier := $2::text, - payload := $3::json, - schedule_info := $4::json, - job_key := $5::text, - queue_name := $6::text, - max_attempts := $7::integer, - priority := $8::integer + identifier := $1::text, + payload := $2::json, + schedule_info := $3::json, + job_key := $4::text, + queue_name := $5::text, + max_attempts := $6::integer, + priority := $7::integer )`, [ - database_id, 'my_job', { just: 'run it' }, { start, end, rule: '*/1 * * * *' }, @@ -101,17 +102,15 @@ describe('scheduled jobs', () => { const [result2] = await pg.any( `SELECT * FROM app_jobs.add_scheduled_job( - db_id := $1, - identifier := $2, - payload := $3, - schedule_info := $4, - job_key := $5, - queue_name := $6, - max_attempts := $7, - priority := $8 + identifier := $1, + payload := $2, + schedule_info := $3, + job_key := $4, + queue_name := $5, + max_attempts := $6, + priority := $7 )`, [ - database_id, 'my_job', { just: 'run it' }, { start, end, rule: '*/1 * * * *' }, diff --git a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap index 669f4b628..bdab1fc59 100644 --- a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap +++ b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap @@ -5,17 +5,19 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` "moduleNames": [ "billing_module", "billing_provider_module", + "config_secrets_org_module", + "config_secrets_user_module", "connected_accounts_module", "crypto_addresses_module", "crypto_auth_module", "default_ids_module", "devices_module", "emails_module", - "encrypted_secrets_module", + "events_module", "hierarchy_module", "identity_providers_module", + "inference_log_module", "invites_module", - "levels_module", "limits_module", "membership_types_module", "memberships_module", @@ -24,14 +26,15 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` "phone_numbers_module", "plans_module", "profiles_module", + "rate_limit_meters_module", "rate_limits_module", "realtime_module", "rls_module", - "secrets_module", "session_secrets_module", "sessions_module", "storage_module", "user_auth_module", + "user_state_module", "users_module", "webauthn_auth_module", "webauthn_credentials_module", @@ -41,8 +44,8 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` exports[`db_meta_modules should verify all module tables exist in metaschema_modules_public schema 1`] = ` { - "moduleTablesCount": 32, - "totalTables": 39, + "moduleTablesCount": 35, + "totalTables": 42, } `; @@ -97,13 +100,13 @@ exports[`db_meta_modules should verify emails_module table structure 1`] = ` exports[`db_meta_modules should verify module table structures have database_id foreign keys 1`] = ` { - "constraintCount": 165888, + "constraintCount": 196875, } `; exports[`db_meta_modules should verify module tables have proper foreign key relationships 1`] = ` { - "constraintCount": 238227, + "constraintCount": 286927, "foreignTables": [ "database", "field", diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql index 7a71abb5b..d5683b397 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/blueprint/table.sql @@ -11,11 +11,6 @@ SELECT description, definition, template_id, - status, - constructed_at, - error_details, - ref_map, - constructed_definition, definition_hash, table_hashes, created_at, 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 index d6bf096f3..0caae8560 100644 --- 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 @@ -18,8 +18,9 @@ SELECT source_field_name, target_field_name, use_composite_key, - node_type, - node_data, + create_index, + expose_in_api, + nodes, grants, policies, out_field_id, diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql index b22b45203..f953e5071 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/secure_table_provision/table.sql @@ -8,9 +8,8 @@ SELECT schema_id, table_id, table_name, - node_type, + nodes, use_rls, - node_data, fields, grants, policies,