diff --git a/packages/database-jobs/Makefile b/packages/database-jobs/Makefile index 755d2e61..a7d586e4 100644 --- a/packages/database-jobs/Makefile +++ b/packages/database-jobs/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-database-jobs -DATA = sql/pgpm-database-jobs--0.26.2.sql +DATA = sql/pgpm-database-jobs--0.26.3.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/database-jobs/package.json b/packages/database-jobs/package.json index 30a5b6d0..7e8794b1 100644 --- a/packages/database-jobs/package.json +++ b/packages/database-jobs/package.json @@ -35,4 +35,4 @@ "bugs": { "url": "https://github.com/constructive-io/pgpm-modules/issues" } -} +} \ No newline at end of file diff --git a/packages/database-jobs/pgpm-database-jobs.control b/packages/database-jobs/pgpm-database-jobs.control index 73a7778b..22f6e534 100644 --- a/packages/database-jobs/pgpm-database-jobs.control +++ b/packages/database-jobs/pgpm-database-jobs.control @@ -1,6 +1,6 @@ # pgpm-database-jobs extension comment = 'pgpm-database-jobs extension' -default_version = '0.26.2' +default_version = '0.26.3' module_pathname = '$libdir/pgpm-database-jobs' requires = 'plpgsql,pgcrypto,pgpm-verify,pgpm-jwt-claims' relocatable = false diff --git a/packages/database-jobs/sql/pgpm-database-jobs--0.26.2.sql b/packages/database-jobs/sql/pgpm-database-jobs--0.26.3.sql similarity index 100% rename from packages/database-jobs/sql/pgpm-database-jobs--0.26.2.sql rename to packages/database-jobs/sql/pgpm-database-jobs--0.26.3.sql diff --git a/packages/encrypted-secrets-table/Makefile b/packages/encrypted-secrets-table/Makefile index 75a049d2..4df48480 100644 --- a/packages/encrypted-secrets-table/Makefile +++ b/packages/encrypted-secrets-table/Makefile @@ -1,5 +1,5 @@ EXTENSION = pgpm-encrypted-secrets-table -DATA = sql/pgpm-encrypted-secrets-table--0.15.5.sql +DATA = sql/pgpm-encrypted-secrets-table--0.26.0.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/encrypted-secrets-table/package.json b/packages/encrypted-secrets-table/package.json index 453905ce..be70849e 100644 --- a/packages/encrypted-secrets-table/package.json +++ b/packages/encrypted-secrets-table/package.json @@ -34,4 +34,4 @@ "bugs": { "url": "https://github.com/constructive-io/pgpm-modules/issues" } -} +} \ No newline at end of file diff --git a/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control b/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control index 9a646ee3..13a73a19 100644 --- a/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control +++ b/packages/encrypted-secrets-table/pgpm-encrypted-secrets-table.control @@ -1,6 +1,6 @@ # pgpm-encrypted-secrets-table extension comment = 'pgpm-encrypted-secrets-table extension' -default_version = '0.15.5' +default_version = '0.26.0' module_pathname = '$libdir/pgpm-encrypted-secrets-table' requires = 'pgcrypto,plpgsql,pgpm-verify' relocatable = false diff --git a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.3.sql b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.3.sql deleted file mode 100644 index a9c317cf..00000000 --- a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.3.sql +++ /dev/null @@ -1,44 +0,0 @@ -\echo Use "CREATE EXTENSION pgpm-encrypted-secrets-table" to load this file. \quit -CREATE SCHEMA secrets_schema; - -CREATE TABLE secrets_schema.secrets_table ( - id uuid PRIMARY KEY DEFAULT uuid_generate_v4(), - secrets_owned_field uuid NOT NULL, - name text NOT NULL, - secrets_value_field bytea NULL, - secrets_enc_field text NULL, - UNIQUE (secrets_owned_field, name) -); - -COMMENT ON TABLE secrets_schema.secrets_table IS 'Encrypted key-value secret storage: stores secrets as either raw bytea or encrypted text, scoped to an owning entity'; -COMMENT ON COLUMN secrets_schema.secrets_table.id IS 'Unique identifier for this secret'; -COMMENT ON COLUMN secrets_schema.secrets_table.secrets_owned_field IS 'UUID of the owning entity (e.g. user, organization); combined with name forms a unique key'; -COMMENT ON COLUMN secrets_schema.secrets_table.name IS 'Name/key for this secret within its owner scope'; -COMMENT ON COLUMN secrets_schema.secrets_table.secrets_value_field IS 'Raw binary secret value (mutually exclusive with secrets_enc_field)'; -COMMENT ON COLUMN secrets_schema.secrets_table.secrets_enc_field IS 'Encrypted text secret value (mutually exclusive with secrets_value_field)'; - -CREATE FUNCTION secrets_schema.tg_hash_secrets() RETURNS trigger AS $EOFCODE$ -BEGIN - IF (NEW.secrets_enc_field = 'crypt') THEN - NEW.secrets_value_field = crypt(NEW.secrets_value_field::text, gen_salt('bf')); - ELSIF (NEW.secrets_enc_field = 'pgp') THEN - NEW.secrets_value_field = pgp_sym_encrypt(encode(NEW.secrets_value_field::bytea, 'hex'), NEW.secrets_owned_field::text, 'compress-algo=1, cipher-algo=aes256'); - ELSE - NEW.secrets_enc_field = 'none'; - END IF; - RETURN NEW; -END; -$EOFCODE$ LANGUAGE plpgsql VOLATILE; - -CREATE TRIGGER hash_secrets_update - BEFORE UPDATE - ON secrets_schema.secrets_table - FOR EACH ROW - WHEN (new.secrets_value_field IS DISTINCT FROM old.secrets_value_field) - EXECUTE PROCEDURE secrets_schema.tg_hash_secrets(); - -CREATE TRIGGER hash_secrets_insert - BEFORE INSERT - ON secrets_schema.secrets_table - FOR EACH ROW - EXECUTE PROCEDURE secrets_schema.tg_hash_secrets(); diff --git a/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.5.sql b/packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.26.0.sql similarity index 100% rename from packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.15.5.sql rename to packages/encrypted-secrets-table/sql/pgpm-encrypted-secrets-table--0.26.0.sql diff --git a/packages/inflection-db/Makefile b/packages/inflection-db/Makefile index 11b619df..3c2804a6 100644 --- a/packages/inflection-db/Makefile +++ b/packages/inflection-db/Makefile @@ -1,5 +1,5 @@ -EXTENSION = pgpm-inflection-db -DATA = sql/pgpm-inflection-db--0.26.0.sql +EXTENSION = inflection-db +DATA = sql/inflection-db--0.26.4.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/inflection-db/__tests__/inflection_db.test.ts b/packages/inflection-db/__tests__/inflection_db/inflection_db.test.ts similarity index 86% rename from packages/inflection-db/__tests__/inflection_db.test.ts rename to packages/inflection-db/__tests__/inflection_db/inflection_db.test.ts index 706087ff..2ed83b12 100644 --- a/packages/inflection-db/__tests__/inflection_db.test.ts +++ b/packages/inflection-db/__tests__/inflection_db/inflection_db.test.ts @@ -223,43 +223,3 @@ cases( ] ); -cases( - 'get_namespace_name', - async (opts: { name: string; parts: string[]; result: string }) => { - const [{ get_namespace_name }] = await pg.any( - 'SELECT * FROM inflection_db.get_namespace_name($1::text[])', - [opts.parts] - ); - expect(get_namespace_name).toEqual(opts.result); - }, - [ - { - name: 'simple', - parts: ['constructive', 'myApp'], - result: 'constructive_my_app' - }, - { - name: 'single', - parts: ['test'], - result: 'test' - } - ] -); - -cases( - 'get_schema_name', - async (opts: { name: string; parts: string[]; result: string }) => { - const [{ get_schema_name }] = await pg.any( - 'SELECT * FROM inflection_db.get_schema_name($1::text[])', - [opts.parts] - ); - expect(get_schema_name).toEqual(opts.result); - }, - [ - { - name: 'simple', - parts: ['my', 'schema'], - result: 'my-schema' - } - ] -); diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_check_constraint_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_check_constraint_name.sql index cdc70894..a1de9be7 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_check_constraint_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_check_constraint_name.sql @@ -18,4 +18,4 @@ $$ LANGUAGE 'sql' STABLE; -COMMIT; +COMMIT; \ No newline at end of file diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_field_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_field_name.sql index 6285835f..4c2573f0 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_field_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_field_name.sql @@ -21,3 +21,4 @@ $$ LANGUAGE 'sql' IMMUTABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_field_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_field_name.sql index 3a6108fb..0b9cffe4 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_field_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_field_name.sql @@ -19,4 +19,4 @@ $$ LANGUAGE 'sql' IMMUTABLE; -COMMIT; +COMMIT; \ No newline at end of file diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_index_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_index_name.sql index 15410ee7..283dac0d 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_index_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_foreign_key_index_name.sql @@ -14,3 +14,4 @@ $$ LANGUAGE 'sql' STABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_identifier_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_identifier_name.sql index c84c24e6..23a7a085 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_identifier_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_identifier_name.sql @@ -21,3 +21,4 @@ $$ LANGUAGE 'sql' IMMUTABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_index_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_index_name.sql index a75e6e4c..c4aaf105 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_index_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_index_name.sql @@ -14,3 +14,4 @@ $$ LANGUAGE 'sql' STABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_primary_key_index_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_primary_key_index_name.sql index 11176081..5f8bfa0e 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_primary_key_index_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_primary_key_index_name.sql @@ -14,3 +14,4 @@ $$ LANGUAGE 'sql' STABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_schema_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_schema_name.sql index 995cc613..40b2e9c9 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_schema_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_schema_name.sql @@ -12,3 +12,4 @@ $$ LANGUAGE 'sql' VOLATILE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_name.sql index fec738f4..fe98598a 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_name.sql @@ -1,24 +1,25 @@ -- Deploy schemas/inflection_db/procedures/get_table_name to pg - -- requires: schemas/inflection_db/schema -- requires: schemas/inflection_db/procedures/get_identifier BEGIN; - -CREATE FUNCTION inflection_db.get_table_name( - table_name text -) RETURNS text AS $$ +CREATE FUNCTION inflection_db.get_table_name (table_name text) + RETURNS text + AS $$ SELECT inflection_db.get_identifier (inflection.plural (inflection.underscore (table_name))); $$ -LANGUAGE 'sql' IMMUTABLE; +LANGUAGE 'sql' +IMMUTABLE; -CREATE FUNCTION inflection_db.get_table_name( - parts text[] -) RETURNS text AS $$ +CREATE FUNCTION inflection_db.get_table_name (parts text[]) + RETURNS text + AS $$ SELECT inflection_db.get_identifier (inflection.plural (inflection.underscore (parts))); $$ -LANGUAGE 'sql' IMMUTABLE; +LANGUAGE 'sql' +IMMUTABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_plural_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_plural_name.sql index b05fda18..e16a1b21 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_plural_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_plural_name.sql @@ -12,3 +12,4 @@ $$ LANGUAGE 'sql' IMMUTABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_singular_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_singular_name.sql index d5e0ccd9..0d47d7a6 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_singular_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_table_singular_name.sql @@ -12,3 +12,4 @@ $$ LANGUAGE 'sql' IMMUTABLE; COMMIT; + diff --git a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_unique_index_name.sql b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_unique_index_name.sql index 305a9b64..30c86484 100644 --- a/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_unique_index_name.sql +++ b/packages/inflection-db/deploy/schemas/inflection_db/procedures/get_unique_index_name.sql @@ -4,6 +4,7 @@ -- requires: schemas/inflection_db/procedures/get_identifier BEGIN; +-- ${table_name}_${field1}_${field2}_${fieldN}_key CREATE FUNCTION inflection_db.get_unique_index_name (table_name text, fields text[]) RETURNS text AS $$ @@ -16,3 +17,4 @@ $$ LANGUAGE 'sql' STABLE; COMMIT; + diff --git a/packages/inflection-db/inflection-db.control b/packages/inflection-db/inflection-db.control new file mode 100644 index 00000000..4b003035 --- /dev/null +++ b/packages/inflection-db/inflection-db.control @@ -0,0 +1,7 @@ +# inflection-db extension +comment = 'inflection-db extension' +default_version = '0.26.4' +module_pathname = '$libdir/inflection-db' +requires = 'pgpm-inflection,pgpm-verify,plpgsql' +relocatable = false +superuser = false diff --git a/packages/inflection-db/jest.config.js b/packages/inflection-db/jest.config.js index 4108cf46..32bd72df 100644 --- a/packages/inflection-db/jest.config.js +++ b/packages/inflection-db/jest.config.js @@ -1,16 +1,19 @@ /** @type {import('ts-jest').JestConfigWithTsJest} */ module.exports = { forceExit: true, - preset: 'ts-jest', - testEnvironment: 'node', - - // Match both __tests__ and colocated test files - testMatch: ['**/?(*.)+(test|spec).{ts,tsx,js,jsx}'], - - // Ignore build artifacts and type declarations - testPathIgnorePatterns: ['/dist/', '\\.d\\.ts$'], - modulePathIgnorePatterns: ['/dist/'], - watchPathIgnorePatterns: ['/dist/'], - - moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], + preset: "ts-jest", + testEnvironment: "node", + transform: { + "^.+\\.tsx?$": [ + "ts-jest", + { + babelConfig: false, + tsconfig: "tsconfig.json", + }, + ], + }, + transformIgnorePatterns: [`/node_modules/*`], + testRegex: "(/__tests__/.*|(\\.|/)(test|spec))\\.(jsx?|tsx?)$", + moduleFileExtensions: ["ts", "tsx", "js", "jsx", "json", "node"], + modulePathIgnorePatterns: ["dist/*"] }; diff --git a/packages/inflection-db/package.json b/packages/inflection-db/package.json index 59634252..2b0dcbca 100644 --- a/packages/inflection-db/package.json +++ b/packages/inflection-db/package.json @@ -36,4 +36,4 @@ "bugs": { "url": "https://github.com/constructive-io/pgpm-modules/issues" } -} +} \ No newline at end of file diff --git a/packages/inflection-db/pgpm-inflection-db.control b/packages/inflection-db/pgpm-inflection-db.control deleted file mode 100644 index 48aa212d..00000000 --- a/packages/inflection-db/pgpm-inflection-db.control +++ /dev/null @@ -1,7 +0,0 @@ -# pgpm-inflection-db extension -comment = 'pgpm-inflection-db extension' -default_version = '0.26.0' -module_pathname = '$libdir/pgpm-inflection-db' -requires = 'pgpm-inflection,pgpm-verify,plpgsql' -relocatable = false -superuser = false diff --git a/packages/inflection-db/pgpm.plan b/packages/inflection-db/pgpm.plan index b84238f1..00e1b3aa 100644 --- a/packages/inflection-db/pgpm.plan +++ b/packages/inflection-db/pgpm.plan @@ -1,7 +1,7 @@ %syntax-version=1.0.0 -%project=pgpm-inflection-db -%uri=pgpm-inflection-db - +%project=inflection-db +%uri=inflection-db + schemas/inflection_db/schema [pgpm-inflection:schemas/inflection/tables/inflection_rules/indexes/inflection_rules_type_idx pgpm-verify:procedures/verify_view] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier [schemas/inflection_db/schema] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_identifier schemas/inflection_db/procedures/get_table_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_table_name @@ -16,4 +16,4 @@ schemas/inflection_db/procedures/get_schema_name [schemas/inflection_db/schema s schemas/inflection_db/procedures/get_table_plural_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_table_plural_name schemas/inflection_db/procedures/get_table_singular_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_table_singular_name schemas/inflection_db/procedures/get_unique_index_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_table_name schemas/inflection_db/procedures/get_identifier] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_unique_index_name -schemas/inflection_db/procedures/get_namespace_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier] 2017-08-11T08:11:51Z skitch # add schemas/inflection_db/procedures/get_namespace_name +schemas/inflection_db/procedures/get_namespace_name [schemas/inflection_db/schema schemas/inflection_db/procedures/get_identifier] 2026-05-21T00:00:00Z devin # add schemas/inflection_db/procedures/get_namespace_name \ No newline at end of file diff --git a/packages/inflection-db/sql/pgpm-inflection-db--0.26.0.sql b/packages/inflection-db/sql/inflection-db--0.26.4.sql similarity index 60% rename from packages/inflection-db/sql/pgpm-inflection-db--0.26.0.sql rename to packages/inflection-db/sql/inflection-db--0.26.4.sql index f3b00ba0..f516df80 100644 --- a/packages/inflection-db/sql/pgpm-inflection-db--0.26.0.sql +++ b/packages/inflection-db/sql/inflection-db--0.26.4.sql @@ -1,4 +1,4 @@ -\echo Use "CREATE EXTENSION pgpm-inflection-db" to load this file. \quit +\echo Use "CREATE EXTENSION inflection-db" to load this file. \quit CREATE SCHEMA inflection_db; GRANT USAGE ON SCHEMA inflection_db TO PUBLIC; @@ -6,30 +6,21 @@ GRANT USAGE ON SCHEMA inflection_db TO PUBLIC; ALTER DEFAULT PRIVILEGES IN SCHEMA inflection_db GRANT EXECUTE ON FUNCTIONS TO PUBLIC; -CREATE FUNCTION inflection_db.get_identifier( - str text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_identifier(str text) RETURNS text AS $EOFCODE$ select substring(str FROM 1 FOR 63); $EOFCODE$ LANGUAGE sql STABLE; -CREATE FUNCTION inflection_db.get_table_name( - table_name text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_table_name(table_name text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.plural (inflection.underscore (table_name))); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_table_name( - parts text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_table_name(parts text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.plural (inflection.underscore (parts))); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_check_constraint_name( - table_name text, - fields text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_check_constraint_name(table_name text, fields text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier ( inflection.underscore ( @@ -37,23 +28,17 @@ CREATE FUNCTION inflection_db.get_check_constraint_name( ); $EOFCODE$ LANGUAGE sql STABLE; -CREATE FUNCTION inflection_db.get_field_name( - field_name text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_field_name(field_name text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (field_name)); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_field_name( - parts text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_field_name(parts text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (parts)); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_foreign_key_field_name( - table_name text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_foreign_key_field_name(table_name text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier ( array_to_string( @@ -62,68 +47,47 @@ CREATE FUNCTION inflection_db.get_foreign_key_field_name( ); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_foreign_key_index_name( - table_name text, - field text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_foreign_key_index_name(table_name text, field text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (array_to_string(ARRAY[inflection_db.get_table_name (table_name)] || ARRAY[field] || ARRAY['fkey'], '_'))); $EOFCODE$ LANGUAGE sql STABLE; -CREATE FUNCTION inflection_db.get_identifier_name( - field_name text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_identifier_name(field_name text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (field_name)); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_identifier_name( - parts text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_identifier_name(parts text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (parts)); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_index_name( - table_name text, - fields text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_index_name(table_name text, fields text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (array_to_string(ARRAY[inflection_db.get_table_name (table_name)] || fields || ARRAY['idx'], '_'))); $EOFCODE$ LANGUAGE sql STABLE; -CREATE FUNCTION inflection_db.get_primary_key_index_name( - table_name text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_primary_key_index_name(table_name text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (array_to_string(ARRAY[inflection_db.get_table_name (table_name)] || ARRAY['pkey'], '_'))); $EOFCODE$ LANGUAGE sql STABLE; -CREATE FUNCTION inflection_db.get_schema_name( - parts text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_schema_name(parts text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.dashed (array_to_string(parts, '-'))); $EOFCODE$ LANGUAGE sql VOLATILE; -CREATE FUNCTION inflection_db.get_table_plural_name( - table_name text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_table_plural_name(table_name text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.plural (inflection.underscore (table_name))); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_table_singular_name( - table_name text -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_table_singular_name(table_name text) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.singular (inflection.underscore (table_name))); $EOFCODE$ LANGUAGE sql IMMUTABLE; -CREATE FUNCTION inflection_db.get_unique_index_name( - table_name text, - fields text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_unique_index_name(table_name text, fields text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier ( inflection.underscore ( @@ -131,9 +95,7 @@ CREATE FUNCTION inflection_db.get_unique_index_name( ); $EOFCODE$ LANGUAGE sql STABLE; -CREATE FUNCTION inflection_db.get_namespace_name( - parts text[] -) RETURNS text AS $EOFCODE$ +CREATE FUNCTION inflection_db.get_namespace_name(parts text[]) RETURNS text AS $EOFCODE$ SELECT inflection_db.get_identifier (inflection.underscore (parts)); $EOFCODE$ LANGUAGE sql IMMUTABLE; \ No newline at end of file diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql index d49e6d93..5cfcfbf8 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_check_constraint_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_check_constraint_name on pg +-- Verify schemas/inflection_db/procedures/get_check_constraint_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql index 0ede9164..8f927a91 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_field_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_field_name on pg +-- Verify schemas/inflection_db/procedures/get_field_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql index abbd2a23..0dd1c6b3 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_field_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_foreign_key_field_name on pg +-- Verify schemas/inflection_db/procedures/get_foreign_key_field_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql index 09942e27..dbdf08bf 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_foreign_key_index_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_foreign_key_index_name on pg +-- Verify schemas/inflection_db/procedures/get_foreign_key_index_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql index e8444ade..8c779243 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_identifier on pg +-- Verify schemas/inflection_db/procedures/get_identifier on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql index 99f79a68..92e85845 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_identifier_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_identifier_name on pg +-- Verify schemas/inflection_db/procedures/get_identifier_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql index 9d95a919..feccab1d 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_index_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_index_name on pg +-- Verify schemas/inflection_db/procedures/get_index_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql index c6c45b78..315c8d0d 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_primary_key_index_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_primary_key_index_name on pg +-- Verify schemas/inflection_db/procedures/get_primary_key_index_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql index fcfea061..7751df55 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_schema_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_schema_name on pg +-- Verify schemas/inflection_db/procedures/get_schema_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql index c7f017c2..1c334b8f 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_table_name on pg +-- Verify schemas/inflection_db/procedures/get_table_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql index af4714c2..b497b616 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_plural_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_table_plural_name on pg +-- Verify schemas/inflection_db/procedures/get_table_plural_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql index 61dac093..1d0d64bc 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_table_singular_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_table_singular_name on pg +-- Verify schemas/inflection_db/procedures/get_table_singular_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql index 5eb0d0fe..a9b3df68 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/procedures/get_unique_index_name.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/procedures/get_unique_index_name on pg +-- Verify schemas/inflection_db/procedures/get_unique_index_name on pg BEGIN; diff --git a/packages/inflection-db/verify/schemas/inflection_db/schema.sql b/packages/inflection-db/verify/schemas/inflection_db/schema.sql index 6fb3e363..77a5645d 100644 --- a/packages/inflection-db/verify/schemas/inflection_db/schema.sql +++ b/packages/inflection-db/verify/schemas/inflection_db/schema.sql @@ -1,4 +1,4 @@ --- Verify schemas/inflection_db/schema on pg +-- Verify schemas/inflection_db/schema on pg BEGIN; diff --git a/packages/metaschema-modules/Makefile b/packages/metaschema-modules/Makefile index f92ca4ec..f8bc992d 100644 --- a/packages/metaschema-modules/Makefile +++ b/packages/metaschema-modules/Makefile @@ -1,5 +1,5 @@ EXTENSION = metaschema-modules -DATA = sql/metaschema-modules--0.26.1.sql +DATA = sql/metaschema-modules--0.26.3.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap index ebfe3afa..6634a250 100644 --- a/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap +++ b/packages/metaschema-modules/__tests__/__snapshots__/modules.test.ts.snap @@ -7,6 +7,7 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` "billing_module", "billing_provider_module", "compute_log_module", + "config_secrets_module", "config_secrets_org_module", "config_secrets_user_module", "connected_accounts_module", @@ -20,6 +21,7 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` "function_module", "graph_module", "hierarchy_module", + "i18n_module", "identity_providers_module", "inference_log_module", "invites_module", @@ -43,6 +45,8 @@ exports[`db_meta_modules should have all expected module tables 1`] = ` "storage_module", "transfer_log_module", "user_auth_module", + "user_credentials_module", + "user_settings_module", "user_state_module", "users_module", "webauthn_auth_module", @@ -53,8 +57,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": 44, - "totalTables": 51, + "moduleTablesCount": 48, + "totalTables": 55, } `; @@ -103,19 +107,31 @@ exports[`db_meta_modules should verify emails_module table structure 1`] = ` "data_type": "text", "is_nullable": "NO", }, + { + "column_default": "'auth'::text", + "column_name": "api_name", + "data_type": "text", + "is_nullable": "YES", + }, + { + "column_default": null, + "column_name": "private_api_name", + "data_type": "text", + "is_nullable": "YES", + }, ], } `; exports[`db_meta_modules should verify module table structures have database_id foreign keys 1`] = ` { - "constraintCount": 269004, + "constraintCount": 303413, } `; exports[`db_meta_modules should verify module tables have proper foreign key relationships 1`] = ` { - "constraintCount": 396560, + "constraintCount": 452625, "foreignTables": [ "database", "field", @@ -244,6 +260,10 @@ exports[`db_meta_modules should verify specific module table column defaults 1`] }, ], "usersDefaults": [ + { + "column_default": "'auth'::text", + "column_name": "api_name", + }, { "column_default": "uuidv7()", "column_name": "id", @@ -317,6 +337,18 @@ exports[`db_meta_modules should verify users_module table structure 1`] = ` "data_type": "text", "is_nullable": "NO", }, + { + "column_default": "'auth'::text", + "column_name": "api_name", + "data_type": "text", + "is_nullable": "YES", + }, + { + "column_default": null, + "column_name": "private_api_name", + "data_type": "text", + "is_nullable": "YES", + }, ], } `; diff --git a/packages/metaschema-modules/__tests__/modules.test.ts b/packages/metaschema-modules/__tests__/modules.test.ts index 70be2d69..43611086 100644 --- a/packages/metaschema-modules/__tests__/modules.test.ts +++ b/packages/metaschema-modules/__tests__/modules.test.ts @@ -169,7 +169,7 @@ describe('db_meta_modules', () => { } expect(snapshot({ constraintCount: constraints.length })).toMatchSnapshot(); - }); + }, 30000); it('should verify all module tables exist in metaschema_modules_public schema', async () => { const tables = await pg.any(` diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/agent_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/agent_module/table.sql index c4ae0822..777deca2 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/agent_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/agent_module/table.sql @@ -17,43 +17,49 @@ CREATE TABLE metaschema_modules_public.agent_module ( message_table_id uuid NOT NULL DEFAULT uuid_nil(), task_table_id uuid NOT NULL DEFAULT uuid_nil(), prompts_table_id uuid NOT NULL DEFAULT uuid_nil(), - knowledge_table_id uuid DEFAULT NULL, + plan_table_id uuid DEFAULT NULL, + agent_table_id uuid DEFAULT NULL, + persona_table_id uuid DEFAULT NULL, + resource_table_id uuid DEFAULT NULL, -- Table names (input to the generator) thread_table_name text NOT NULL DEFAULT 'agent_thread', message_table_name text NOT NULL DEFAULT 'agent_message', task_table_name text NOT NULL DEFAULT 'agent_task', prompts_table_name text NOT NULL DEFAULT 'agent_prompt', - knowledge_table_name text NOT NULL DEFAULT 'agent_knowledge', + plan_table_name text NOT NULL DEFAULT 'agent_plan', + agent_table_name text NOT NULL DEFAULT 'agent', + persona_table_name text NOT NULL DEFAULT 'agent_persona', + resource_table_name text NOT NULL DEFAULT 'agent_resource', -- Feature flags - has_knowledge boolean NOT NULL DEFAULT false, + has_plans boolean NOT NULL DEFAULT false, + has_resources boolean NOT NULL DEFAULT false, + has_agents boolean NOT NULL DEFAULT false, + shared boolean NOT NULL DEFAULT false, - -- API routing (get-or-create: if set, schema is added to this API; if NULL, no API is added) + -- API routing (configurable per-module) api_name text DEFAULT 'agent', + private_api_name text DEFAULT NULL, - -- Multi-tenant scope - membership_type int DEFAULT NULL, + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', - -- Module key discriminator: allows multiple agent modules per scope. - -- 'default' is omitted from table names, any other value becomes - -- an infix: {prefix}_{key}_agent_thread. - -- Max 16 chars, lowercase snake_case. - key text NOT NULL DEFAULT 'default', + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) entity_table_id uuid NULL, - -- Configurable security policies (NULL = use defaults based on membership_type) + -- Configurable security policies (NULL = use defaults based on scope) policies jsonb NULL, - -- Knowledge RAG config (dimensions, chunk_size, chunk_strategy, search_indexes, etc.) - -- NULL = use sensible defaults (768d, 1000 chars, paragraph, bm25) - knowledge_config jsonb NULL, - - -- Custom RLS policies for knowledge table (provisions.knowledge.policies) - -- NULL = use defaults (AuthzEntityMembership or AuthzAppMembership) - knowledge_policies jsonb NULL, + -- Resource configuration array (dimensions, chunk_size, chunk_strategy, etc.) + -- NULL = use sensible defaults (768d, 1000 chunk_size, paragraph strategy) + -- Example: [{"dimensions": 1536, "chunk_size": 500, "chunk_strategy": "sentence"}] + resources jsonb NULL, -- Per-table provisions overrides from blueprint config. -- Keys are table keys (thread, message, task, prompt, knowledge). @@ -61,6 +67,10 @@ CREATE TABLE metaschema_modules_public.agent_module ( -- secure_table_provision applies the custom grants/policies instead. provisions jsonb NULL, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + -- Constraints CONSTRAINT agent_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT agent_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -69,15 +79,16 @@ CREATE TABLE metaschema_modules_public.agent_module ( CONSTRAINT agent_module_message_table_fkey FOREIGN KEY (message_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT agent_module_task_table_fkey FOREIGN KEY (task_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT agent_module_prompts_table_fkey FOREIGN KEY (prompts_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT agent_module_knowledge_table_fkey FOREIGN KEY (knowledge_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_plan_table_fkey FOREIGN KEY (plan_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_agent_table_fkey FOREIGN KEY (agent_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_persona_table_fkey FOREIGN KEY (persona_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT agent_module_resource_table_fkey FOREIGN KEY (resource_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT agent_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); CREATE INDEX agent_module_database_id_idx ON metaschema_modules_public.agent_module ( database_id ); --- Unique constraint on (database_id, membership_type, key) using COALESCE to handle NULLs. --- NULL membership_type = app-level, non-NULL = entity-scoped. key discriminates --- multiple agent modules for the same scope (e.g. 'support' + 'internal'). -CREATE UNIQUE INDEX agent_module_unique_scope ON metaschema_modules_public.agent_module ( database_id, COALESCE(membership_type, -1), key ); +-- Unique constraint: one agent module per database per scope per prefix. +CREATE UNIQUE INDEX agent_module_unique_scope ON metaschema_modules_public.agent_module ( database_id, scope, prefix ); COMMIT; 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 00dcdef9..4d2aa0b5 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 @@ -35,11 +35,23 @@ CREATE TABLE metaschema_modules_public.billing_module ( meter_sources_table_id uuid NOT NULL DEFAULT uuid_nil(), meter_sources_table_name text NOT NULL DEFAULT '', + -- Meter defaults table: app-scope default meter catalog seeded at provision time + meter_defaults_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_defaults_table_name text NOT NULL DEFAULT '', + -- Generated functions record_usage_function text NOT NULL DEFAULT '', prefix text NULL, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -49,6 +61,7 @@ CREATE TABLE metaschema_modules_public.billing_module ( 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 meter_sources_table_fkey FOREIGN KEY (meter_sources_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT meter_defaults_table_fkey FOREIGN KEY (meter_defaults_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/billing_provider_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql index 5c4d14d5..22adc393 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/billing_provider_module/table.sql @@ -42,6 +42,10 @@ CREATE TABLE metaschema_modules_public.billing_provider_module ( prefix text NULL, + -- API routing (configurable per-module) + api_name text DEFAULT NULL, + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/compute_log_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/compute_log_module/table.sql index 6f79fcf3..cda421e2 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/compute_log_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/compute_log_module/table.sql @@ -24,12 +24,17 @@ CREATE TABLE metaschema_modules_public.compute_log_module ( retention text NOT NULL DEFAULT '12 months', premake int NOT NULL DEFAULT 2, - -- Scope configuration: 'app' = per-app usage (actor_id RLS), 'platform' = tenant metering (database_id RLS) + -- Scope configuration: 'app' = per-app usage (actor_id RLS) scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql new file mode 100644 index 00000000..9d320395 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql @@ -0,0 +1,82 @@ +-- Deploy schemas/metaschema_modules_public/tables/config_secrets_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.config_secrets_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (resolved by BEFORE INSERT trigger when uuid_nil) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Generated table IDs (populated by the generator) + table_id uuid NOT NULL DEFAULT uuid_nil(), + config_definitions_table_id uuid NULL DEFAULT NULL, + + -- Table name (input — bare name without scope prefix). + -- The trigger prepends the scope prefix automatically. + table_name text NOT NULL DEFAULT 'secrets', + + -- API routing (get-or-create: if set, schema is added to this API) + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + -- 'app' = app-level (AuthzAppMembership, admin-only secrets + config) + -- 'org' = org-scoped (AuthzEntityMembership, per-org secrets with manage_secrets permission) + -- custom entity type names for entity-scoped secrets + -- Note: user-scoped credentials are handled by user_credentials_module (separate module) + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + + -- Configurable security policies (NULL = use defaults based on scope). + -- When provided, replaces the default policy set in the security function. + -- Accepts a JSON array of policy objects: + -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} + policies jsonb NULL, + + -- Per-table provisions overrides from blueprint config. + -- Keys are table keys (secrets, config_definitions). + -- When a key is present, the module trigger skips default security for that table; + -- secure_table_provision applies the custom grants/policies instead. + provisions jsonb NULL, + + -- Feature flags + + -- When true, also creates a plaintext config table ({prefix}_config) and + -- a config definitions registry table ({prefix}_config_definitions). + -- Only meaningful for app-level scope (scope = 'app'). + has_config boolean NOT NULL DEFAULT false, + + -- Constraints + CONSTRAINT config_secrets_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_config_defs_table_fkey FOREIGN KEY (config_definitions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT config_secrets_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX config_secrets_module_database_id_idx ON metaschema_modules_public.config_secrets_module ( database_id ); +CREATE INDEX config_secrets_module_schema_id_idx ON metaschema_modules_public.config_secrets_module ( schema_id ); +CREATE INDEX config_secrets_module_table_id_idx ON metaschema_modules_public.config_secrets_module ( table_id ); + +-- Unique constraint: one config_secrets module per database per scope per prefix. +CREATE UNIQUE INDEX config_secrets_module_unique_scope ON metaschema_modules_public.config_secrets_module ( database_id, scope, prefix ); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_module IS + 'Entity-aware PGP-encrypted key-value config/secrets module. Supports app-level (admin-only) + and org-scoped (per-org secrets with manage_secrets permission) via the scope column. + User-scoped bcrypt credentials are handled by user_credentials_module.'; + +COMMIT; 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 index dba9b605..0a9465b3 100644 --- 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 @@ -14,6 +14,10 @@ CREATE TABLE metaschema_modules_public.config_secrets_org_module ( table_name text NOT NULL DEFAULT 'org_secrets', -- + -- API routing (configurable per-module) + api_name text DEFAULT 'config', + private_api_name 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, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql index 3213f873..e0e6c5a2 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/config_secrets_user_module/table.sql @@ -17,6 +17,10 @@ CREATE TABLE metaschema_modules_public.config_secrets_user_module ( config_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), -- + -- API routing (configurable per-module) + api_name text DEFAULT 'config', + private_api_name 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, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql index 05823df8..6f831d86 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/connected_accounts_module/table.sql @@ -16,6 +16,10 @@ CREATE TABLE metaschema_modules_public.connected_accounts_module ( table_name text NOT NULL, + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql index 394f63fb..7e3747fd 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/crypto_addresses_module/table.sql @@ -17,6 +17,10 @@ CREATE TABLE metaschema_modules_public.crypto_addresses_module ( table_name text NOT NULL, crypto_network text NOT NULL DEFAULT 'BTC', + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/db_usage_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/db_usage_module/table.sql index 2ce00247..9a667fef 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/db_usage_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/db_usage_module/table.sql @@ -32,10 +32,19 @@ CREATE TABLE metaschema_modules_public.db_usage_module ( retention text NOT NULL DEFAULT '12 months', premake int NOT NULL DEFAULT 2, - -- Scope configuration: 'app' = per-app usage, 'platform' = tenant metering (database_id RLS) + -- Scope configuration: 'app' = per-app usage scope text NOT NULL DEFAULT 'app', - prefix text NULL, + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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-modules/deploy/schemas/metaschema_modules_public/tables/emails_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/emails_module/table.sql index 346629b3..ee831229 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/emails_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/emails_module/table.sql @@ -16,6 +16,10 @@ CREATE TABLE metaschema_modules_public.emails_module ( table_name text NOT NULL, + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, 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 112f00fc..d5285446 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 @@ -137,10 +137,6 @@ CREATE TABLE metaschema_modules_public.entity_type_provision ( out_secret_definitions_table_id uuid DEFAULT NULL, - out_requirements_table_id uuid DEFAULT NULL, - - out_config_requirements_table_id uuid DEFAULT NULL, - out_graph_module_id uuid DEFAULT NULL, out_graphs_table_id uuid DEFAULT NULL, 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 index 5e368c9a..d363250c 100644 --- 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 @@ -54,17 +54,27 @@ CREATE TABLE metaschema_modules_public.events_module ( retention text DEFAULT '12 months', premake int NOT NULL DEFAULT 2, - prefix text NULL, + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', - 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 + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) entity_table_id uuid NULL, -- required tables actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/function_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/function_module/table.sql index ee11c67c..841d5345 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/function_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/function_module/table.sql @@ -21,9 +21,6 @@ CREATE TABLE metaschema_modules_public.function_module ( invocations_table_id uuid NOT NULL DEFAULT uuid_nil(), execution_logs_table_id uuid NOT NULL DEFAULT uuid_nil(), secret_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), - requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), - config_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), - config_requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), -- Table names (input to the generator — bare names without scope prefix). -- The trigger prepends the scope prefix automatically. @@ -31,43 +28,38 @@ CREATE TABLE metaschema_modules_public.function_module ( invocations_table_name text NOT NULL DEFAULT 'function_invocations', execution_logs_table_name text NOT NULL DEFAULT 'function_execution_logs', secret_definitions_table_name text NOT NULL DEFAULT 'secret_definitions', - requirements_table_name text NOT NULL DEFAULT 'function_secret_requirements', - config_requirements_table_name text NOT NULL DEFAULT 'function_config_requirements', -- API routing (get-or-create: if set, schema is added to this API; if NULL, no API is added) api_name text, private_api_name text, - -- Multi-tenant function identity - membership_type int DEFAULT NULL, -- NULL = database-root (AuthzMembership via app_sprt), non-NULL = entity-scoped (AuthzEntityMembership) + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', - -- Scope prefix for table naming. Auto-derived from membership_type when - -- NULL: NULL/1 → 'app', 2 → 'org'. Can be overridden explicitly. - -- The trigger prepends this to all bare table names - -- (e.g. prefix='app' + 'function_definitions' → 'app_function_definitions'). - prefix text NULL, - - -- Module key discriminator: allows multiple function modules per scope. - -- 'default' is omitted from table names, any other value becomes - -- an infix: {prefix}_{key}_function_definitions. - -- Max 16 chars, lowercase snake_case. - key text NOT NULL DEFAULT 'default', + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', -- Entity table for RLS (NULL for app-level functions, entity table for entity-scoped functions) entity_table_id uuid NULL, - -- Configurable security policies (NULL = use defaults based on membership_type). + -- Configurable security policies (NULL = use defaults based on scope). -- When provided, replaces the default policy set in apply_function_security. -- Accepts a JSON array of policy objects: -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} policies jsonb NULL, -- Per-table provisions overrides from blueprint config. - -- Keys are table keys (definitions, invocations, execution_logs, secret_definitions, requirements). + -- Keys are table keys (definitions, invocations, execution_logs, secret_definitions). -- When a key is present, the module trigger skips default security for that table; -- secure_table_provision applies the custom grants/policies instead. provisions jsonb NULL, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + -- Constraints CONSTRAINT function_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT function_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -76,17 +68,12 @@ CREATE TABLE metaschema_modules_public.function_module ( CONSTRAINT function_module_invocations_table_fkey FOREIGN KEY (invocations_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT function_module_execution_logs_table_fkey FOREIGN KEY (execution_logs_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT function_module_secret_defs_table_fkey FOREIGN KEY (secret_definitions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT function_module_requirements_table_fkey FOREIGN KEY (requirements_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT function_module_config_defs_table_fkey FOREIGN KEY (config_definitions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT function_module_config_reqs_table_fkey FOREIGN KEY (config_requirements_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT function_module_entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); CREATE INDEX function_module_database_id_idx ON metaschema_modules_public.function_module ( database_id ); --- Unique constraint on (database_id, membership_type, key) using COALESCE to handle NULLs. --- NULL membership_type = app-level, non-NULL = entity-scoped. key discriminates --- multiple function modules for the same scope (e.g. 'webhooks' + 'automations'). -CREATE UNIQUE INDEX function_module_unique_scope ON metaschema_modules_public.function_module ( database_id, COALESCE(membership_type, -1), key ); +-- Unique constraint: one function module per database per scope per prefix. +CREATE UNIQUE INDEX function_module_unique_scope ON metaschema_modules_public.function_module ( database_id, scope, prefix ); COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/graph_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/graph_module/table.sql index 90f6101c..1eaf0750 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/graph_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/graph_module/table.sql @@ -17,8 +17,10 @@ CREATE TABLE metaschema_modules_public.graph_module ( public_schema_name text, private_schema_name text, - -- Table/function prefix (e.g., 'pipeline' -> pipeline_function_graphs, ...) - -- Stored normalized (no trailing underscore); underscore added at generation time + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. prefix text NOT NULL DEFAULT '', -- Reference to the Merkle store this graph module depends on @@ -33,12 +35,8 @@ CREATE TABLE metaschema_modules_public.graph_module ( api_name text, private_api_name text, - -- Scope field name (column used for multi-tenant isolation) - scope_field text NOT NULL DEFAULT 'scope_id', - - -- Multi-tenant scoping (entity-aware module pattern) - membership_type int DEFAULT NULL, -- NULL = database-root, non-NULL = entity-scoped - entity_table_id uuid NULL, -- Entity table for entity-scoped RLS + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, -- Configurable security policies (NULL = use defaults). -- Accepts a JSON array of policy objects: @@ -51,6 +49,10 @@ CREATE TABLE metaschema_modules_public.graph_module ( -- secure_table_provision applies the custom grants/policies instead. provisions jsonb NULL, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + -- Timestamps created_at timestamptz NOT NULL DEFAULT now(), diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql index e4241c25..d85ad586 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/hierarchy_module/table.sql @@ -23,11 +23,14 @@ CREATE TABLE metaschema_modules_public.hierarchy_module ( chart_edge_grants_table_name text NOT NULL DEFAULT '', -- Required external table references - entity_table_id uuid NOT NULL, -- Organizations table (membership_type=2 entity) + entity_table_id uuid NOT NULL, -- Organizations table (entity-scoped) users_table_id uuid NOT NULL, -- Users table + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'org', - -- Prefix for naming (e.g., 'org' -> 'org_chart_edges') - prefix text NOT NULL DEFAULT 'org', + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', -- Resolved names for RLS parser lookups private_schema_name text NOT NULL DEFAULT '', @@ -39,6 +42,10 @@ CREATE TABLE metaschema_modules_public.hierarchy_module ( get_managers_function text NOT NULL DEFAULT '', is_manager_of_function text NOT NULL DEFAULT '', + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + -- Timestamps created_at timestamptz NOT NULL DEFAULT now(), diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/i18n_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/i18n_module/table.sql new file mode 100644 index 00000000..df567812 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/i18n_module/table.sql @@ -0,0 +1,31 @@ +-- Deploy schemas/metaschema_modules_public/tables/i18n_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.i18n_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (populated by the insert trigger) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + private_schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Settings table (populated by the generator) + settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- API routing (configurable per-module) + api_name text DEFAULT NULL, + private_api_name 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, + CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT settings_table_fkey FOREIGN KEY (settings_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX i18n_module_database_id_idx ON metaschema_modules_public.i18n_module ( database_id ); +CREATE UNIQUE INDEX i18n_module_unique_per_db ON metaschema_modules_public.i18n_module ( database_id ); + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql index e87355c7..ae017f4d 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/identity_providers_module/table.sql @@ -15,11 +15,30 @@ CREATE TABLE metaschema_modules_public.identity_providers_module ( table_name text NOT NULL DEFAULT 'identity_providers', - -- + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + + -- Entity-aware scope: determines which config_secrets_module table + -- the rotate_identity_provider_{prefix}_secret proc targets. + -- 'app' = app_secrets (AuthzAppMembership, admin-only) + -- 'org' = org_secrets (AuthzEntityMembership, per-org) + -- Future entity types are also supported via the membership_types table. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix for the generated rotate proc. + -- Auto-derived from scope by the trigger when empty. + -- e.g. scope='app' → prefix='app' → rotate_identity_provider_app_secret + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) + entity_table_id uuid NULL, + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, - CONSTRAINT private_schema_fkey FOREIGN KEY (private_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 entity_table_fkey FOREIGN KEY (entity_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE ); CREATE INDEX identity_providers_module_database_id_idx ON metaschema_modules_public.identity_providers_module ( database_id ); @@ -27,7 +46,20 @@ CREATE INDEX identity_providers_module_schema_id_idx ON metaschema_modules_publi CREATE INDEX identity_providers_module_private_schema_id_idx ON metaschema_modules_public.identity_providers_module ( private_schema_id ); CREATE INDEX identity_providers_module_table_id_idx ON metaschema_modules_public.identity_providers_module ( table_id ); -COMMENT ON TABLE metaschema_modules_public.identity_providers_module IS 'Config row for the identity_providers_module, which provisions a per-database identity_providers config table holding OAuth2 / OIDC (and future SAML) provider definitions: protocol kind, endpoint URLs, encrypted client secret, scopes, audience validation, PKCE, and email-handling flags. Built-in providers (google, github, apple, ...) are seeded as is_built_in=true rows; custom providers use slugs of the form custom:.'; +-- One install per database per scope +CREATE UNIQUE INDEX identity_providers_module_unique_scope ON metaschema_modules_public.identity_providers_module ( database_id, scope ); + +COMMENT ON TABLE metaschema_modules_public.identity_providers_module IS + 'Entity-aware config row for the identity_providers_module, which provisions a per-database + identity_providers table holding OAuth2 / OIDC (and future SAML) provider definitions. + The scope column determines which config_secrets_module table the rotate proc targets + (app_secrets for app scope, org_secrets for org scope). When scope = platform, + the secrets table gets a database_id column and platform-level RLS via + AuthzRelatedEntityMembership through database.owner_id. + Scoping matrix: + scope=app → per-database flat, in-app admin manages + scope=platform → per-database, platform admin manages (generate:constructive) + scope=org → per-org tenant, org admin manages'; COMMENT ON COLUMN metaschema_modules_public.identity_providers_module.private_schema_id IS 'Private schema that hosts SECURITY DEFINER admin helpers which write to identity_providers (create / update / enable / disable / rotate-secret / delete) and the per-app quota check.'; 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 index fa4a830f..8c94221b 100644 --- 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 @@ -24,12 +24,17 @@ CREATE TABLE metaschema_modules_public.inference_log_module ( retention text NOT NULL DEFAULT '12 months', premake int NOT NULL DEFAULT 2, - -- Scope configuration: 'app' = per-app usage (actor_id RLS), 'platform' = tenant metering (database_id RLS) + -- Scope configuration: 'app' = per-app usage (actor_id RLS) scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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-modules/deploy/schemas/metaschema_modules_public/tables/invites_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/invites_module/table.sql index b6f0bc27..8a1a1297 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/invites_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/invites_module/table.sql @@ -21,13 +21,19 @@ CREATE TABLE metaschema_modules_public.invites_module ( claimed_invites_table_name text NOT NULL DEFAULT '', submit_invite_code_function text NOT NULL DEFAULT '', - prefix text NULL, + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', - 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 + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) entity_table_id uuid NULL, + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT invites_table_fkey FOREIGN KEY (invites_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, @@ -41,12 +47,8 @@ CREATE TABLE metaschema_modules_public.invites_module ( CREATE INDEX invites_module_database_id_idx ON metaschema_modules_public.invites_module ( database_id ); --- Unique constraint on (database_id, membership_type) so the --- entity_type_provision fan-out can use ON CONFLICT DO NOTHING for idempotent --- re-provisioning. invites_module is always entity-scoped (membership_type --- is NOT NULL on this table), so no COALESCE-NULL trick is needed here, --- unlike storage_module which supports an app-level singleton row. +-- Unique constraint: one invites module per database per scope per prefix. CREATE UNIQUE INDEX invites_module_unique_scope - ON metaschema_modules_public.invites_module (database_id, membership_type); + ON metaschema_modules_public.invites_module (database_id, scope, prefix); 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 ef7246b1..bde28379 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 @@ -61,16 +61,22 @@ CREATE TABLE metaschema_modules_public.limits_module ( limit_check_soft_function text NOT NULL DEFAULT '', limit_aggregate_check_soft_function text NOT NULL DEFAULT '', - prefix text NULL, + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', - 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 + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) entity_table_id uuid NULL, -- required tables actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/memberships_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/memberships_module/table.sql index 9dc86683..fa07cdfb 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/memberships_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/memberships_module/table.sql @@ -41,15 +41,16 @@ CREATE TABLE metaschema_modules_public.memberships_module ( owner_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), owner_grants_table_name text NOT NULL DEFAULT '', - membership_type int NOT NULL, + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', - -- if this is NOT NULL, then we add entity_id - -- e.g. memberships to the app itself are considered global owned by app and no explicit owner + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) entity_table_id uuid NULL, entity_table_owner_id uuid NULL, - prefix text NULL, - -- Populated by memberships_module generator when get_organization_id is created get_org_fn text NULL, @@ -65,6 +66,10 @@ CREATE TABLE metaschema_modules_public.memberships_module ( -- + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -83,7 +88,9 @@ CREATE TABLE metaschema_modules_public.memberships_module ( CONSTRAINT default_limits_table_fkey FOREIGN KEY (default_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT permissions_table_fkey FOREIGN KEY (permissions_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) ON DELETE CASCADE + CONSTRAINT default_permissions_table_fkey FOREIGN KEY (default_permissions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + + CONSTRAINT memberships_module_unique UNIQUE (database_id, scope, prefix) ); CREATE INDEX memberships_module_database_id_idx ON metaschema_modules_public.memberships_module ( database_id ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql index fd0f3426..744c5f66 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/merkle_store_module/table.sql @@ -30,8 +30,9 @@ CREATE TABLE metaschema_modules_public.merkle_store_module ( api_name text, private_api_name text, - -- Scope field name (column used for multi-tenant isolation) - scope_field text NOT NULL DEFAULT 'scope_id', + -- Scope: 'app' for app-level, 'platform' for database-scoped with + -- RLS through metaschema_public.database ownership. + scope text NOT NULL DEFAULT 'app', -- Timestamps created_at timestamptz NOT NULL DEFAULT now(), diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/namespace_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/namespace_module/table.sql index 29ffd082..2015322a 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/namespace_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/namespace_module/table.sql @@ -28,19 +28,18 @@ CREATE TABLE metaschema_modules_public.namespace_module ( api_name text, private_api_name text, - -- Multi-tenant namespace identity - membership_type int DEFAULT NULL, -- NULL = database-root (AuthzMembership via app_sprt), non-NULL = entity-scoped (AuthzEntityMembership) + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', - -- Module key discriminator: allows multiple namespace modules per scope. - -- 'default' is omitted from table names, any other value becomes - -- an infix: {prefix}_{key}_namespaces. - -- Max 16 chars, lowercase snake_case. - key text NOT NULL DEFAULT 'default', + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', -- Entity table for RLS (NULL for app-level namespaces, entity table for entity-scoped namespaces) entity_table_id uuid NULL, - -- Configurable security policies (NULL = use defaults based on membership_type). + -- Configurable security policies (NULL = use defaults based on scope). -- When provided, replaces the default policy set in apply_namespace_security. -- Accepts a JSON array of policy objects: -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} @@ -52,6 +51,10 @@ CREATE TABLE metaschema_modules_public.namespace_module ( -- secure_table_provision applies the custom grants/policies instead. provisions jsonb NULL, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + -- Constraints CONSTRAINT namespace_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT namespace_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -63,9 +66,7 @@ CREATE TABLE metaschema_modules_public.namespace_module ( CREATE INDEX namespace_module_database_id_idx ON metaschema_modules_public.namespace_module ( database_id ); --- Unique constraint on (database_id, membership_type, key) using COALESCE to handle NULLs. --- NULL membership_type = app-level, non-NULL = entity-scoped. key discriminates --- multiple namespace modules for the same scope (e.g. 'config' + 'content'). -CREATE UNIQUE INDEX namespace_module_unique_scope ON metaschema_modules_public.namespace_module ( database_id, COALESCE(membership_type, -1), key ); +-- Unique constraint: one namespace module per database per scope per prefix. +CREATE UNIQUE INDEX namespace_module_unique_scope ON metaschema_modules_public.namespace_module ( database_id, scope, prefix ); COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/notifications_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/notifications_module/table.sql index 0feaa811..0acd075b 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/notifications_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/notifications_module/table.sql @@ -36,6 +36,14 @@ CREATE TABLE metaschema_modules_public.notifications_module ( has_digest_metadata boolean NOT NULL DEFAULT false, has_subscriptions boolean NOT NULL DEFAULT false, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'notifications', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT notifications_table_fkey FOREIGN KEY (notifications_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/permissions_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/permissions_module/table.sql index 8d3ddae3..9017fe42 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/permissions_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/permissions_module/table.sql @@ -42,16 +42,19 @@ CREATE TABLE metaschema_modules_public.permissions_module ( -- Existing databases are unaffected; this only changes the default for -- newly inserted permissions_module rows. bitlen int NOT NULL DEFAULT 64, - 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 + + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- Entity table for RLS (NULL for app-level, entity table for entity-scoped) entity_table_id uuid NULL, -- required tables actor_table_id uuid NOT NULL DEFAULT uuid_nil(), - prefix text NULL, - -- get_padded_mask text NOT NULL DEFAULT '', @@ -61,6 +64,10 @@ CREATE TABLE metaschema_modules_public.permissions_module ( -- + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql index 66053dc6..35db3eb8 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/phone_numbers_module/table.sql @@ -16,6 +16,10 @@ CREATE TABLE metaschema_modules_public.phone_numbers_module ( table_name text NOT NULL, + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/plans_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/plans_module/table.sql index 55d5d0a7..3f51ff9a 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/plans_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/plans_module/table.sql @@ -25,12 +25,26 @@ CREATE TABLE metaschema_modules_public.plans_module ( -- Plan overrides table: per-entity custom limit overrides plan_overrides_table_id uuid NULL, + -- Plan meter limits table: maps plan → meter slug → billing quota + plan_meter_limits_table_id uuid NULL, + + -- Plan caps table: maps plan → cap name → cap value (feature flags) + plan_caps_table_id uuid NULL, + -- Generated apply_plan functions (one per limits scope) apply_plan_function text NOT NULL DEFAULT '', apply_plan_aggregate_function text NOT NULL DEFAULT '', + -- Generated apply functions for billing and caps (set when respective modules are installed) + apply_billing_plan_function text NULL, + apply_plan_caps_function text NULL, + prefix text NULL, + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -38,6 +52,8 @@ CREATE TABLE metaschema_modules_public.plans_module ( CONSTRAINT plan_limits_table_fkey FOREIGN KEY (plan_limits_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) ON DELETE CASCADE, CONSTRAINT plan_overrides_table_fkey FOREIGN KEY (plan_overrides_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_meter_limits_table_fkey FOREIGN KEY (plan_meter_limits_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, + CONSTRAINT plan_caps_table_fkey FOREIGN KEY (plan_caps_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT plans_module_database_id_unique UNIQUE (database_id) ); diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql index a0461a57..13728055 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/profiles_module/table.sql @@ -31,7 +31,11 @@ CREATE TABLE metaschema_modules_public.profiles_module ( profile_templates_table_id uuid NOT NULL DEFAULT uuid_nil(), profile_templates_table_name text NOT NULL DEFAULT '', - membership_type int NOT NULL, + -- Scope: determines the security level for this module instance. + scope text NOT NULL DEFAULT 'app', + + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', -- Entity table for org/group scoped profiles (NULL for app-level) entity_table_id uuid NULL, @@ -41,8 +45,10 @@ CREATE TABLE metaschema_modules_public.profiles_module ( permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), memberships_table_id uuid NOT NULL DEFAULT uuid_nil(), - prefix text NULL, - + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name 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, @@ -57,7 +63,7 @@ CREATE TABLE metaschema_modules_public.profiles_module ( CONSTRAINT permissions_table_fkey FOREIGN KEY (permissions_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, CONSTRAINT memberships_table_fkey FOREIGN KEY (memberships_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, - CONSTRAINT profiles_module_unique UNIQUE (database_id, membership_type) + CONSTRAINT profiles_module_unique UNIQUE (database_id, scope, prefix) ); CREATE INDEX profiles_module_database_id_idx ON metaschema_modules_public.profiles_module ( database_id ); 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 index 268a3522..984e7f47 100644 --- 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 @@ -30,6 +30,14 @@ CREATE TABLE metaschema_modules_public.rate_limit_meters_module ( prefix text NULL, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, 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 a521e560..41973324 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 @@ -27,6 +27,10 @@ CREATE TABLE metaschema_modules_public.realtime_module ( notify_channel text NULL, -- Constraints + -- API routing (configurable per-module) + api_name text DEFAULT 'realtime', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql index 11264ca0..ef6d8bf0 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/rls_module/table.sql @@ -22,6 +22,10 @@ CREATE TABLE metaschema_modules_public.rls_module ( "current_role" text NOT NULL DEFAULT 'current_user', current_role_id text NOT NULL DEFAULT 'current_user_id', + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT session_credentials_table_fkey FOREIGN KEY (session_credentials_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_log_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_log_module/table.sql index 77d0388f..e11ecd67 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_log_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/storage_log_module/table.sql @@ -24,12 +24,17 @@ CREATE TABLE metaschema_modules_public.storage_log_module ( retention text NOT NULL DEFAULT '12 months', premake int NOT NULL DEFAULT 2, - -- Scope configuration: 'app' = per-app usage (actor_id RLS), 'platform' = tenant metering (database_id RLS) + -- Scope configuration: 'app' = per-app usage (actor_id RLS) scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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-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 dd3041bf..52c907ca 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 @@ -17,19 +17,18 @@ CREATE TABLE metaschema_modules_public.storage_module ( files_table_id uuid NOT NULL DEFAULT uuid_nil(), -- Table names (input to the generator) - buckets_table_name text NOT NULL DEFAULT 'app_buckets', - files_table_name text NOT NULL DEFAULT 'app_files', + buckets_table_name text NOT NULL DEFAULT 'buckets', + files_table_name text NOT NULL DEFAULT 'files', - -- Multi-tenant storage identity - membership_type int DEFAULT NULL, -- NULL = global gate (AuthzMembership via app_sprt), non-NULL = entity-scoped (AuthzEntityMembership) + -- Scope: determines the security level for this module instance. + -- Resolved to a membership_type integer at trigger time via membership_types table. + scope text NOT NULL DEFAULT 'app', - -- Module key discriminator: allows multiple storage modules per entity type. - -- 'default' is omitted from table names (backward compat), any other value becomes - -- an infix: {prefix}_{key}_{buckets|files}. - -- Max 16 chars, lowercase snake_case, cannot be 'buckets'/'files'/'bucket'/'file'. - key text NOT NULL DEFAULT 'default', + -- Table name prefix. Auto-derived from scope by the trigger when empty. + -- Override to create multiple module instances at the same scope. + prefix text NOT NULL DEFAULT '', - -- Configurable security policies (NULL = use defaults based on membership_type). + -- Configurable security policies (NULL = use defaults based on scope). -- When provided, replaces the default policy set in apply_storage_security. -- Accepts a JSON array of policy objects: -- {"$type": "AuthzEntityMembership", "privileges": ["select", "update"], "data": {...}} @@ -87,7 +86,15 @@ CREATE TABLE metaschema_modules_public.storage_module ( -- Generated table ID for file_events (populated by the generator when has_audit_log=true) file_events_table_id uuid NULL DEFAULT NULL, + -- Default permissions: permission names auto-granted to new members. + -- NULL uses the module's built-in defaults; explicit array overrides them. + default_permissions text[] DEFAULT NULL, + -- Constraints + -- API routing (configurable per-module) + api_name text DEFAULT 'admin', + private_api_name 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, CONSTRAINT private_schema_fkey FOREIGN KEY (private_schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, @@ -100,9 +107,7 @@ 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, key) using COALESCE to handle NULLs. --- NULL membership_type = app-level, non-NULL = entity-scoped. 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), key ); +-- Unique constraint: one storage module per database per scope per prefix. +CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module ( database_id, scope, prefix ); COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql index 145863e4..05d1a9c7 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/transfer_log_module/table.sql @@ -24,12 +24,17 @@ CREATE TABLE metaschema_modules_public.transfer_log_module ( retention text NOT NULL DEFAULT '12 months', premake int NOT NULL DEFAULT 2, - -- Scope configuration: 'app' = per-app usage (actor_id RLS), 'platform' = tenant metering (database_id RLS) + -- Scope configuration: 'app' = per-app usage (actor_id RLS) scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + -- Table name prefix. Auto-derived from scope by the trigger when empty. + prefix text NOT NULL DEFAULT '', + + -- API routing (configurable per-module) + api_name text DEFAULT 'usage', + private_api_name 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-modules/deploy/schemas/metaschema_modules_public/tables/user_auth_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_auth_module/table.sql index d4608cb0..3d49988d 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_auth_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_auth_module/table.sql @@ -44,6 +44,10 @@ CREATE TABLE metaschema_modules_public.user_auth_module ( -- UNIQUE(api_id), + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name 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, CONSTRAINT email_table_fkey FOREIGN KEY (emails_table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql new file mode 100644 index 00000000..d5bf3533 --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql @@ -0,0 +1,42 @@ +-- Deploy schemas/metaschema_modules_public/tables/user_credentials_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.user_credentials_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema references (resolved by BEFORE INSERT trigger when uuid_nil) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Generated table ID (populated by the generator) + table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table name (input — defaults to 'user_secrets') + table_name text NOT NULL DEFAULT 'user_secrets', + + -- API routing (get-or-create: if set, schema is added to this API) + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + + -- Constraints + CONSTRAINT user_credentials_module_db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT user_credentials_module_schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + CONSTRAINT user_credentials_module_table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE +); + +CREATE INDEX user_credentials_module_database_id_idx ON metaschema_modules_public.user_credentials_module ( database_id ); +CREATE INDEX user_credentials_module_schema_id_idx ON metaschema_modules_public.user_credentials_module ( schema_id ); +CREATE INDEX user_credentials_module_table_id_idx ON metaschema_modules_public.user_credentials_module ( table_id ); + +-- One user_credentials_module per database. +CREATE UNIQUE INDEX user_credentials_module_unique ON metaschema_modules_public.user_credentials_module ( database_id ); + +COMMENT ON TABLE metaschema_modules_public.user_credentials_module IS + 'Per-user bcrypt credential store (password hashes, API key hashes). + Always user-scoped with AuthzDirectOwner RLS. Consumed by user_auth_module, + identity_providers_module, and bootstrap procedures.'; + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_module/table.sql new file mode 100644 index 00000000..5a50044b --- /dev/null +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/user_settings_module/table.sql @@ -0,0 +1,34 @@ +-- Deploy schemas/metaschema_modules_public/tables/user_settings_module/table to pg + +-- requires: schemas/metaschema_modules_public/schema + +BEGIN; + +CREATE TABLE metaschema_modules_public.user_settings_module ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + + -- Schema reference (populated by the insert trigger) + schema_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Table reference (populated by the generator) + table_id uuid NOT NULL DEFAULT uuid_nil(), + + -- Owner table reference (resolved to users table by trigger) + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + + table_name text NOT NULL DEFAULT 'user_settings', + + -- API routing (configurable per-module) + api_name 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, + 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 +); + +CREATE INDEX user_settings_module_database_id_idx ON metaschema_modules_public.user_settings_module ( database_id ); +CREATE UNIQUE INDEX user_settings_module_unique_per_db ON metaschema_modules_public.user_settings_module ( database_id ); + +COMMIT; diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/users_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/users_module/table.sql index c736df90..8a9a727b 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/users_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/users_module/table.sql @@ -18,6 +18,10 @@ CREATE TABLE metaschema_modules_public.users_module ( type_table_name text NOT NULL DEFAULT 'role_types', -- + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name 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, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql index f8118b80..f540f4b4 100644 --- a/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql +++ b/packages/metaschema-modules/deploy/schemas/metaschema_modules_public/tables/webauthn_credentials_module/table.sql @@ -16,6 +16,10 @@ CREATE TABLE metaschema_modules_public.webauthn_credentials_module ( table_name text NOT NULL DEFAULT 'webauthn_credentials', + -- API routing (configurable per-module) + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + -- CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, CONSTRAINT table_fkey FOREIGN KEY (table_id) REFERENCES metaschema_public.table (id) ON DELETE CASCADE, diff --git a/packages/metaschema-modules/metaschema-modules.control b/packages/metaschema-modules/metaschema-modules.control index 1320071b..f7dd5703 100644 --- a/packages/metaschema-modules/metaschema-modules.control +++ b/packages/metaschema-modules/metaschema-modules.control @@ -1,6 +1,6 @@ # metaschema-modules extension comment = 'metaschema-modules extension' -default_version = '0.26.1' +default_version = '0.26.3' module_pathname = '$libdir/metaschema-modules' requires = 'plpgsql,uuid-ossp,metaschema-schema,services,pgpm-verify' relocatable = false diff --git a/packages/metaschema-modules/package.json b/packages/metaschema-modules/package.json index 56625401..0929026a 100644 --- a/packages/metaschema-modules/package.json +++ b/packages/metaschema-modules/package.json @@ -35,4 +35,4 @@ "bugs": { "url": "https://github.com/constructive-io/pgpm-modules/issues" } -} +} \ No newline at end of file diff --git a/packages/metaschema-modules/pgpm.plan b/packages/metaschema-modules/pgpm.plan index aa0f5293..ba706dd3 100644 --- a/packages/metaschema-modules/pgpm.plan +++ b/packages/metaschema-modules/pgpm.plan @@ -60,4 +60,8 @@ schemas/metaschema_modules_public/tables/merkle_store_module/table [schemas/meta schemas/metaschema_modules_public/tables/graph_module/table [schemas/metaschema_modules_public/schema schemas/metaschema_modules_public/tables/merkle_store_module/table] 2026-05-21T01:00:00Z devin # add graph_module config table for FBP graph utilities on top of merkle store schemas/metaschema_modules_public/tables/namespace_module/table [schemas/metaschema_modules_public/schema] 2026-05-21T02:00:00Z devin # add namespace_module config table for entity-aware namespace provisioning schemas/metaschema_modules_public/tables/function_module/table [schemas/metaschema_modules_public/schema] 2026-05-21T03:00:00Z devin # add function_module config table for entity-aware function definitions +schemas/metaschema_modules_public/tables/config_secrets_module/table [schemas/metaschema_modules_public/schema] 2026-05-29T00:00:00Z devin # add entity-aware config_secrets_module (replaces config_secrets_user_module + config_secrets_org_module) +schemas/metaschema_modules_public/tables/user_credentials_module/table [schemas/metaschema_modules_public/schema] 2026-05-30T00:00:00Z devin # add user_credentials_module for per-user bcrypt credential store (split from config_secrets_module) +schemas/metaschema_modules_public/tables/user_settings_module/table [schemas/metaschema_modules_public/schema] 2026-05-28T00:00:00Z devin # add user_settings_module for extensible per-user preferences (1:1 with users) +schemas/metaschema_modules_public/tables/i18n_module/table [schemas/metaschema_modules_public/schema] 2026-05-28T00:00:00Z devin # add i18n_module config table for internationalization settings diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_chat_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_chat_module/table.sql new file mode 100644 index 00000000..c5a40cd9 --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/agent_chat_module/table.sql @@ -0,0 +1,3 @@ +-- Revert schemas/metaschema_modules_public/tables/agent_module/table from pg + +DROP TABLE IF EXISTS metaschema_modules_public.agent_module; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql new file mode 100644 index 00000000..4a00859e --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/config_secrets_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.config_secrets_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/i18n_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/i18n_module/table.sql new file mode 100644 index 00000000..63e7cdfe --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/i18n_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/i18n_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.i18n_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql new file mode 100644 index 00000000..bca115ea --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_credentials_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.user_credentials_module; + +COMMIT; diff --git a/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_module/table.sql b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_module/table.sql new file mode 100644 index 00000000..95138ae6 --- /dev/null +++ b/packages/metaschema-modules/revert/schemas/metaschema_modules_public/tables/user_settings_module/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_modules_public/tables/user_settings_module/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_modules_public.user_settings_module; + +COMMIT; diff --git a/packages/metaschema-modules/sql/metaschema-modules--0.26.1.sql b/packages/metaschema-modules/sql/metaschema-modules--0.26.3.sql similarity index 91% rename from packages/metaschema-modules/sql/metaschema-modules--0.26.1.sql rename to packages/metaschema-modules/sql/metaschema-modules--0.26.3.sql index 17ed0125..91453238 100644 --- a/packages/metaschema-modules/sql/metaschema-modules--0.26.1.sql +++ b/packages/metaschema-modules/sql/metaschema-modules--0.26.3.sql @@ -31,6 +31,8 @@ CREATE TABLE metaschema_modules_public.connected_accounts_module ( table_id uuid NOT NULL DEFAULT uuid_nil(), owner_table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL, + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -64,6 +66,8 @@ CREATE TABLE metaschema_modules_public.crypto_addresses_module ( owner_table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL, crypto_network text NOT NULL DEFAULT 'BTC', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -187,6 +191,8 @@ CREATE TABLE metaschema_modules_public.emails_module ( table_id uuid NOT NULL DEFAULT uuid_nil(), owner_table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL, + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -218,6 +224,8 @@ CREATE TABLE metaschema_modules_public.config_secrets_user_module ( table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL DEFAULT 'user_secrets', config_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -250,9 +258,11 @@ CREATE TABLE metaschema_modules_public.invites_module ( invites_table_name text NOT NULL DEFAULT '', claimed_invites_table_name text NOT NULL DEFAULT '', submit_invite_code_function text NOT NULL DEFAULT '', - prefix text NULL, - membership_type int NOT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -289,7 +299,7 @@ CREATE TABLE metaschema_modules_public.invites_module ( CREATE INDEX invites_module_database_id_idx ON metaschema_modules_public.invites_module (database_id); -CREATE UNIQUE INDEX invites_module_unique_scope ON metaschema_modules_public.invites_module (database_id, membership_type); +CREATE UNIQUE INDEX invites_module_unique_scope ON metaschema_modules_public.invites_module (database_id, scope, prefix); CREATE TABLE metaschema_modules_public.events_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -327,10 +337,13 @@ CREATE TABLE metaschema_modules_public.events_module ( 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, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -412,10 +425,12 @@ CREATE TABLE metaschema_modules_public.limits_module ( 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, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, actor_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -535,10 +550,10 @@ CREATE TABLE metaschema_modules_public.memberships_module ( admin_grants_table_name text NOT NULL DEFAULT '', owner_grants_table_id uuid NOT NULL DEFAULT uuid_nil(), owner_grants_table_name text NOT NULL DEFAULT '', - membership_type int NOT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, entity_table_owner_id uuid NULL, - prefix text NULL, get_org_fn text NULL, actor_mask_check text NOT NULL DEFAULT '', actor_perm_check text NOT NULL DEFAULT '', @@ -546,6 +561,8 @@ CREATE TABLE metaschema_modules_public.memberships_module ( entity_ids_by_perm text NULL, entity_ids_function text NULL, member_profiles_table_id uuid NULL, + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -609,7 +626,9 @@ CREATE TABLE metaschema_modules_public.memberships_module ( CONSTRAINT default_permissions_table_fkey FOREIGN KEY(default_permissions_table_id) REFERENCES metaschema_public."table" (id) - ON DELETE CASCADE + ON DELETE CASCADE, + CONSTRAINT memberships_module_unique + UNIQUE (database_id, scope, prefix) ); CREATE INDEX memberships_module_database_id_idx ON metaschema_modules_public.memberships_module (database_id); @@ -624,14 +643,16 @@ CREATE TABLE metaschema_modules_public.permissions_module ( default_table_id uuid NOT NULL DEFAULT uuid_nil(), default_table_name text NOT NULL DEFAULT '', bitlen int NOT NULL DEFAULT 64, - membership_type int NOT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, actor_table_id uuid NOT NULL DEFAULT uuid_nil(), - prefix text NULL, get_padded_mask text NOT NULL DEFAULT '', get_mask text NOT NULL DEFAULT '', get_by_mask text NOT NULL DEFAULT '', get_mask_by_name text NOT NULL DEFAULT '', + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -672,6 +693,8 @@ CREATE TABLE metaschema_modules_public.phone_numbers_module ( table_id uuid NOT NULL DEFAULT uuid_nil(), owner_table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL, + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -711,12 +734,14 @@ CREATE TABLE metaschema_modules_public.profiles_module ( profile_definition_grants_table_name text NOT NULL DEFAULT '', profile_templates_table_id uuid NOT NULL DEFAULT uuid_nil(), profile_templates_table_name text NOT NULL DEFAULT '', - membership_type int NOT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, actor_table_id uuid NOT NULL DEFAULT uuid_nil(), permissions_table_id uuid NOT NULL DEFAULT uuid_nil(), memberships_table_id uuid NOT NULL DEFAULT uuid_nil(), - prefix text NULL, + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -766,7 +791,7 @@ CREATE TABLE metaschema_modules_public.profiles_module ( REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT profiles_module_unique - UNIQUE (database_id, membership_type) + UNIQUE (database_id, scope, prefix) ); CREATE INDEX profiles_module_database_id_idx ON metaschema_modules_public.profiles_module (database_id); @@ -783,6 +808,8 @@ CREATE TABLE metaschema_modules_public.rls_module ( authenticate_strict text NOT NULL DEFAULT 'authenticate_strict', "current_role" text NOT NULL DEFAULT 'current_user', current_role_id text NOT NULL DEFAULT 'current_user_id', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -916,6 +943,8 @@ CREATE TABLE metaschema_modules_public.user_auth_module ( sign_in_cross_origin_function text NOT NULL DEFAULT 'sign_in_cross_origin', request_cross_origin_token_function text NOT NULL DEFAULT 'request_cross_origin_token', extend_token_expires text NOT NULL DEFAULT 'extend_token_expires', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -972,6 +1001,8 @@ CREATE TABLE metaschema_modules_public.users_module ( table_name text NOT NULL DEFAULT 'users', type_table_id uuid NOT NULL DEFAULT uuid_nil(), type_table_name text NOT NULL DEFAULT 'role_types', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -1005,13 +1036,15 @@ CREATE TABLE metaschema_modules_public.hierarchy_module ( chart_edge_grants_table_name text NOT NULL DEFAULT '', entity_table_id uuid NOT NULL, users_table_id uuid NOT NULL, - prefix text NOT NULL DEFAULT 'org', + scope text NOT NULL DEFAULT 'org', + prefix text NOT NULL DEFAULT '', private_schema_name text NOT NULL DEFAULT '', sprt_table_name text NOT NULL DEFAULT '', rebuild_hierarchy_function text NOT NULL DEFAULT '', get_subordinates_function text NOT NULL DEFAULT '', get_managers_function text NOT NULL DEFAULT '', is_manager_of_function text NOT NULL DEFAULT '', + default_permissions text[] DEFAULT NULL, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT db_fkey FOREIGN KEY(database_id) @@ -1460,10 +1493,10 @@ CREATE TABLE metaschema_modules_public.storage_module ( private_schema_id uuid NOT NULL DEFAULT uuid_nil(), buckets_table_id uuid NOT NULL DEFAULT uuid_nil(), files_table_id uuid NOT NULL DEFAULT uuid_nil(), - buckets_table_name text NOT NULL DEFAULT 'app_buckets', - files_table_name text NOT NULL DEFAULT 'app_files', - membership_type int DEFAULT NULL, - key text NOT NULL DEFAULT 'default', + buckets_table_name text NOT NULL DEFAULT 'buckets', + files_table_name text NOT NULL DEFAULT 'files', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', policies jsonb NULL, provisions jsonb NULL, entity_table_id uuid NULL, @@ -1488,6 +1521,9 @@ CREATE TABLE metaschema_modules_public.storage_module ( 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, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'admin', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -1524,7 +1560,7 @@ CREATE TABLE metaschema_modules_public.storage_module ( 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)), key); +CREATE UNIQUE INDEX storage_module_unique_scope ON metaschema_modules_public.storage_module (database_id, scope, prefix); CREATE TABLE metaschema_modules_public.entity_type_provision ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -1564,8 +1600,6 @@ CREATE TABLE metaschema_modules_public.entity_type_provision ( out_invocations_table_id uuid DEFAULT NULL, out_execution_logs_table_id uuid DEFAULT NULL, out_secret_definitions_table_id uuid DEFAULT NULL, - out_requirements_table_id uuid DEFAULT NULL, - out_config_requirements_table_id uuid DEFAULT NULL, out_graph_module_id uuid DEFAULT NULL, out_graphs_table_id uuid DEFAULT NULL, out_agent_module_id uuid DEFAULT NULL, @@ -1901,6 +1935,8 @@ CREATE TABLE metaschema_modules_public.webauthn_credentials_module ( table_id uuid NOT NULL DEFAULT uuid_nil(), owner_table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL DEFAULT 'webauthn_credentials', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -1989,6 +2025,11 @@ CREATE TABLE metaschema_modules_public.identity_providers_module ( private_schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL DEFAULT 'identity_providers', + api_name text DEFAULT 'auth', + private_api_name text DEFAULT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2004,6 +2045,10 @@ CREATE TABLE metaschema_modules_public.identity_providers_module ( CONSTRAINT private_schema_fkey FOREIGN KEY(private_schema_id) REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT entity_table_fkey + FOREIGN KEY(entity_table_id) + REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); @@ -2015,7 +2060,18 @@ CREATE INDEX identity_providers_module_private_schema_id_idx ON metaschema_modul CREATE INDEX identity_providers_module_table_id_idx ON metaschema_modules_public.identity_providers_module (table_id); -COMMENT ON TABLE metaschema_modules_public.identity_providers_module IS 'Config row for the identity_providers_module, which provisions a per-database identity_providers config table holding OAuth2 / OIDC (and future SAML) provider definitions: protocol kind, endpoint URLs, encrypted client secret, scopes, audience validation, PKCE, and email-handling flags. Built-in providers (google, github, apple, ...) are seeded as is_built_in=true rows; custom providers use slugs of the form custom:.'; +CREATE UNIQUE INDEX identity_providers_module_unique_scope ON metaschema_modules_public.identity_providers_module (database_id, scope); + +COMMENT ON TABLE metaschema_modules_public.identity_providers_module IS 'Entity-aware config row for the identity_providers_module, which provisions a per-database + identity_providers table holding OAuth2 / OIDC (and future SAML) provider definitions. + The scope column determines which config_secrets_module table the rotate proc targets + (app_secrets for app scope, org_secrets for org scope). When scope = platform, + the secrets table gets a database_id column and platform-level RLS via + AuthzRelatedEntityMembership through database.owner_id. + Scoping matrix: + scope=app → per-database flat, in-app admin manages + scope=platform → per-database, platform admin manages (generate:constructive) + scope=org → per-org tenant, org admin manages'; COMMENT ON COLUMN metaschema_modules_public.identity_providers_module.private_schema_id IS 'Private schema that hosts SECURITY DEFINER admin helpers which write to identity_providers (create / update / enable / disable / rotate-secret / delete) and the per-app quota check.'; @@ -2037,6 +2093,9 @@ CREATE TABLE metaschema_modules_public.notifications_module ( has_settings_extension boolean NOT NULL DEFAULT false, has_digest_metadata boolean NOT NULL DEFAULT false, has_subscriptions boolean NOT NULL DEFAULT false, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'notifications', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2125,9 +2184,15 @@ CREATE TABLE metaschema_modules_public.plans_module ( plan_limits_table_name text NOT NULL DEFAULT '', plan_pricing_table_id uuid NULL, plan_overrides_table_id uuid NULL, + plan_meter_limits_table_id uuid NULL, + plan_caps_table_id uuid NULL, apply_plan_function text NOT NULL DEFAULT '', apply_plan_aggregate_function text NOT NULL DEFAULT '', + apply_billing_plan_function text NULL, + apply_plan_caps_function text NULL, prefix text NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2156,6 +2221,14 @@ CREATE TABLE metaschema_modules_public.plans_module ( FOREIGN KEY(plan_overrides_table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, + CONSTRAINT plan_meter_limits_table_fkey + FOREIGN KEY(plan_meter_limits_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT plan_caps_table_fkey + FOREIGN KEY(plan_caps_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, CONSTRAINT plans_module_database_id_unique UNIQUE (database_id) ); @@ -2179,8 +2252,13 @@ CREATE TABLE metaschema_modules_public.billing_module ( meter_credits_table_name text NOT NULL DEFAULT '', meter_sources_table_id uuid NOT NULL DEFAULT uuid_nil(), meter_sources_table_name text NOT NULL DEFAULT '', + meter_defaults_table_id uuid NOT NULL DEFAULT uuid_nil(), + meter_defaults_table_name text NOT NULL DEFAULT '', record_usage_function text NOT NULL DEFAULT '', prefix text NULL, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2217,6 +2295,10 @@ CREATE TABLE metaschema_modules_public.billing_module ( FOREIGN KEY(meter_sources_table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, + CONSTRAINT meter_defaults_table_fkey + FOREIGN KEY(meter_defaults_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, CONSTRAINT billing_module_database_id_unique UNIQUE (database_id) ); @@ -2244,6 +2326,8 @@ CREATE TABLE metaschema_modules_public.billing_provider_module ( billing_webhook_events_table_name text NOT NULL DEFAULT '', process_billing_event_function text NOT NULL DEFAULT '', prefix text NULL, + api_name text DEFAULT NULL, + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2307,6 +2391,8 @@ CREATE TABLE metaschema_modules_public.realtime_module ( premake int NOT NULL DEFAULT 7, interval text NOT NULL DEFAULT '1 day', notify_channel text NULL, + api_name text DEFAULT 'realtime', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2366,6 +2452,9 @@ CREATE TABLE metaschema_modules_public.rate_limit_meters_module ( rate_window_limits_table_name text NOT NULL DEFAULT '', check_rate_limit_function text NOT NULL DEFAULT '', prefix text NULL, + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2408,6 +2497,8 @@ CREATE TABLE metaschema_modules_public.config_secrets_org_module ( schema_id uuid NOT NULL DEFAULT uuid_nil(), table_id uuid NOT NULL DEFAULT uuid_nil(), table_name text NOT NULL DEFAULT 'org_secrets', + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2445,7 +2536,9 @@ CREATE TABLE metaschema_modules_public.inference_log_module ( scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2487,7 +2580,9 @@ CREATE TABLE metaschema_modules_public.compute_log_module ( scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2529,7 +2624,9 @@ CREATE TABLE metaschema_modules_public.transfer_log_module ( scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2571,7 +2668,9 @@ CREATE TABLE metaschema_modules_public.storage_log_module ( scope text NOT NULL DEFAULT 'app', actor_fk_table_id uuid NULL, entity_fk_table_id uuid NULL, - prefix text NULL, + prefix text NOT NULL DEFAULT '', + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2615,7 +2714,10 @@ CREATE TABLE metaschema_modules_public.db_usage_module ( retention text NOT NULL DEFAULT '12 months', premake int NOT NULL DEFAULT 2, scope text NOT NULL DEFAULT 'app', - prefix text NULL, + prefix text NOT NULL DEFAULT '', + default_permissions text[] DEFAULT NULL, + api_name text DEFAULT 'usage', + private_api_name text DEFAULT NULL, CONSTRAINT db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2659,21 +2761,31 @@ CREATE TABLE metaschema_modules_public.agent_module ( message_table_id uuid NOT NULL DEFAULT uuid_nil(), task_table_id uuid NOT NULL DEFAULT uuid_nil(), prompts_table_id uuid NOT NULL DEFAULT uuid_nil(), - knowledge_table_id uuid DEFAULT NULL, + plan_table_id uuid DEFAULT NULL, + agent_table_id uuid DEFAULT NULL, + persona_table_id uuid DEFAULT NULL, + resource_table_id uuid DEFAULT NULL, thread_table_name text NOT NULL DEFAULT 'agent_thread', message_table_name text NOT NULL DEFAULT 'agent_message', task_table_name text NOT NULL DEFAULT 'agent_task', prompts_table_name text NOT NULL DEFAULT 'agent_prompt', - knowledge_table_name text NOT NULL DEFAULT 'agent_knowledge', - has_knowledge boolean NOT NULL DEFAULT false, + plan_table_name text NOT NULL DEFAULT 'agent_plan', + agent_table_name text NOT NULL DEFAULT 'agent', + persona_table_name text NOT NULL DEFAULT 'agent_persona', + resource_table_name text NOT NULL DEFAULT 'agent_resource', + has_plans boolean NOT NULL DEFAULT false, + has_resources boolean NOT NULL DEFAULT false, + has_agents boolean NOT NULL DEFAULT false, + shared boolean NOT NULL DEFAULT false, api_name text DEFAULT 'agent', - membership_type int DEFAULT NULL, - key text NOT NULL DEFAULT 'default', + private_api_name text DEFAULT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, policies jsonb NULL, - knowledge_config jsonb NULL, - knowledge_policies jsonb NULL, + resources jsonb NULL, provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, CONSTRAINT agent_module_db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2702,8 +2814,20 @@ CREATE TABLE metaschema_modules_public.agent_module ( FOREIGN KEY(prompts_table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, - CONSTRAINT agent_module_knowledge_table_fkey - FOREIGN KEY(knowledge_table_id) + CONSTRAINT agent_module_plan_table_fkey + FOREIGN KEY(plan_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_agent_table_fkey + FOREIGN KEY(agent_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_persona_table_fkey + FOREIGN KEY(persona_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE, + CONSTRAINT agent_module_resource_table_fkey + FOREIGN KEY(resource_table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, CONSTRAINT agent_module_entity_table_fkey @@ -2714,7 +2838,7 @@ CREATE TABLE metaschema_modules_public.agent_module ( CREATE INDEX agent_module_database_id_idx ON metaschema_modules_public.agent_module (database_id); -CREATE UNIQUE INDEX agent_module_unique_scope ON metaschema_modules_public.agent_module (database_id, (COALESCE(membership_type, -1)), key); +CREATE UNIQUE INDEX agent_module_unique_scope ON metaschema_modules_public.agent_module (database_id, scope, prefix); CREATE TABLE metaschema_modules_public.merkle_store_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -2730,7 +2854,7 @@ CREATE TABLE metaschema_modules_public.merkle_store_module ( prefix text NOT NULL DEFAULT '', api_name text, private_api_name text, - scope_field text NOT NULL DEFAULT 'scope_id', + scope text NOT NULL DEFAULT 'app', created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT db_fkey FOREIGN KEY(database_id) @@ -2775,6 +2899,7 @@ CREATE TABLE metaschema_modules_public.graph_module ( private_schema_id uuid NOT NULL DEFAULT uuid_nil(), public_schema_name text, private_schema_name text, + scope text NOT NULL DEFAULT 'app', prefix text NOT NULL DEFAULT '', merkle_store_module_id uuid NOT NULL, graphs_table_id uuid NOT NULL DEFAULT uuid_nil(), @@ -2782,11 +2907,10 @@ CREATE TABLE metaschema_modules_public.graph_module ( outputs_table_id uuid NOT NULL DEFAULT uuid_nil(), api_name text, private_api_name text, - scope_field text NOT NULL DEFAULT 'scope_id', - membership_type int DEFAULT NULL, entity_table_id uuid NULL, policies jsonb NULL, provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT db_fkey FOREIGN KEY(database_id) @@ -2839,11 +2963,12 @@ CREATE TABLE metaschema_modules_public.namespace_module ( namespace_events_table_name text NOT NULL DEFAULT 'namespace_events', api_name text, private_api_name text, - membership_type int DEFAULT NULL, - key text NOT NULL DEFAULT 'default', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, policies jsonb NULL, provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, CONSTRAINT namespace_module_db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2872,7 +2997,7 @@ CREATE TABLE metaschema_modules_public.namespace_module ( CREATE INDEX namespace_module_database_id_idx ON metaschema_modules_public.namespace_module (database_id); -CREATE UNIQUE INDEX namespace_module_unique_scope ON metaschema_modules_public.namespace_module (database_id, (COALESCE(membership_type, -1)), key); +CREATE UNIQUE INDEX namespace_module_unique_scope ON metaschema_modules_public.namespace_module (database_id, scope, prefix); CREATE TABLE metaschema_modules_public.function_module ( id uuid PRIMARY KEY DEFAULT uuidv7(), @@ -2885,23 +3010,18 @@ CREATE TABLE metaschema_modules_public.function_module ( invocations_table_id uuid NOT NULL DEFAULT uuid_nil(), execution_logs_table_id uuid NOT NULL DEFAULT uuid_nil(), secret_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), - requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), - config_definitions_table_id uuid NOT NULL DEFAULT uuid_nil(), - config_requirements_table_id uuid NOT NULL DEFAULT uuid_nil(), definitions_table_name text NOT NULL DEFAULT 'function_definitions', invocations_table_name text NOT NULL DEFAULT 'function_invocations', execution_logs_table_name text NOT NULL DEFAULT 'function_execution_logs', secret_definitions_table_name text NOT NULL DEFAULT 'secret_definitions', - requirements_table_name text NOT NULL DEFAULT 'function_secret_requirements', - config_requirements_table_name text NOT NULL DEFAULT 'function_config_requirements', api_name text, private_api_name text, - membership_type int DEFAULT NULL, - prefix text NULL, - key text NOT NULL DEFAULT 'default', + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', entity_table_id uuid NULL, policies jsonb NULL, provisions jsonb NULL, + default_permissions text[] DEFAULT NULL, CONSTRAINT function_module_db_fkey FOREIGN KEY(database_id) REFERENCES metaschema_public.database (id) @@ -2930,24 +3050,160 @@ CREATE TABLE metaschema_modules_public.function_module ( FOREIGN KEY(secret_definitions_table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, - CONSTRAINT function_module_requirements_table_fkey - FOREIGN KEY(requirements_table_id) + CONSTRAINT function_module_entity_table_fkey + FOREIGN KEY(entity_table_id) REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX function_module_database_id_idx ON metaschema_modules_public.function_module (database_id); + +CREATE UNIQUE INDEX function_module_unique_scope ON metaschema_modules_public.function_module (database_id, scope, prefix); + +CREATE TABLE metaschema_modules_public.config_secrets_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(), + config_definitions_table_id uuid NULL DEFAULT NULL, + table_name text NOT NULL DEFAULT 'secrets', + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + scope text NOT NULL DEFAULT 'app', + prefix text NOT NULL DEFAULT '', + entity_table_id uuid NULL, + policies jsonb NULL, + provisions jsonb NULL, + has_config boolean NOT NULL DEFAULT false, + CONSTRAINT config_secrets_module_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) ON DELETE CASCADE, - CONSTRAINT function_module_config_defs_table_fkey - FOREIGN KEY(config_definitions_table_id) + CONSTRAINT config_secrets_module_schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT config_secrets_module_private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT config_secrets_module_table_fkey + FOREIGN KEY(table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, - CONSTRAINT function_module_config_reqs_table_fkey - FOREIGN KEY(config_requirements_table_id) + CONSTRAINT config_secrets_module_config_defs_table_fkey + FOREIGN KEY(config_definitions_table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE, - CONSTRAINT function_module_entity_table_fkey + CONSTRAINT config_secrets_module_entity_table_fkey FOREIGN KEY(entity_table_id) REFERENCES metaschema_public."table" (id) ON DELETE CASCADE ); -CREATE INDEX function_module_database_id_idx ON metaschema_modules_public.function_module (database_id); +CREATE INDEX config_secrets_module_database_id_idx ON metaschema_modules_public.config_secrets_module (database_id); + +CREATE INDEX config_secrets_module_schema_id_idx ON metaschema_modules_public.config_secrets_module (schema_id); + +CREATE INDEX config_secrets_module_table_id_idx ON metaschema_modules_public.config_secrets_module (table_id); + +CREATE UNIQUE INDEX config_secrets_module_unique_scope ON metaschema_modules_public.config_secrets_module (database_id, scope, prefix); + +COMMENT ON TABLE metaschema_modules_public.config_secrets_module IS 'Entity-aware PGP-encrypted key-value config/secrets module. Supports app-level (admin-only) + and org-scoped (per-org secrets with manage_secrets permission) via the scope column. + User-scoped bcrypt credentials are handled by user_credentials_module.'; + +CREATE TABLE metaschema_modules_public.user_credentials_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 'user_secrets', + api_name text DEFAULT 'config', + private_api_name text DEFAULT NULL, + CONSTRAINT user_credentials_module_db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT user_credentials_module_schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT user_credentials_module_table_fkey + FOREIGN KEY(table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX user_credentials_module_database_id_idx ON metaschema_modules_public.user_credentials_module (database_id); + +CREATE INDEX user_credentials_module_schema_id_idx ON metaschema_modules_public.user_credentials_module (schema_id); + +CREATE INDEX user_credentials_module_table_id_idx ON metaschema_modules_public.user_credentials_module (table_id); + +CREATE UNIQUE INDEX user_credentials_module_unique ON metaschema_modules_public.user_credentials_module (database_id); + +COMMENT ON TABLE metaschema_modules_public.user_credentials_module IS 'Per-user bcrypt credential store (password hashes, API key hashes). + Always user-scoped with AuthzDirectOwner RLS. Consumed by user_auth_module, + identity_providers_module, and bootstrap procedures.'; + +CREATE TABLE metaschema_modules_public.user_settings_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(), + owner_table_id uuid NOT NULL DEFAULT uuid_nil(), + table_name text NOT NULL DEFAULT 'user_settings', + api_name 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, + 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 +); + +CREATE INDEX user_settings_module_database_id_idx ON metaschema_modules_public.user_settings_module (database_id); + +CREATE UNIQUE INDEX user_settings_module_unique_per_db ON metaschema_modules_public.user_settings_module (database_id); + +CREATE TABLE metaschema_modules_public.i18n_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(), + settings_table_id uuid NOT NULL DEFAULT uuid_nil(), + api_name text DEFAULT NULL, + private_api_name 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, + CONSTRAINT private_schema_fkey + FOREIGN KEY(private_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + CONSTRAINT settings_table_fkey + FOREIGN KEY(settings_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE CASCADE +); + +CREATE INDEX i18n_module_database_id_idx ON metaschema_modules_public.i18n_module (database_id); -CREATE UNIQUE INDEX function_module_unique_scope ON metaschema_modules_public.function_module (database_id, (COALESCE(membership_type, -1)), key); \ No newline at end of file +CREATE UNIQUE INDEX i18n_module_unique_per_db ON metaschema_modules_public.i18n_module (database_id); \ No newline at end of file diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql new file mode 100644 index 00000000..8ecf487d --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/config_secrets_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/config_secrets_module/table on pg + +BEGIN; + +SELECT verify_table('metaschema_modules_public.config_secrets_module'); + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/i18n_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/i18n_module/table.sql new file mode 100644 index 00000000..2f17fba3 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/i18n_module/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_modules_public/tables/i18n_module/table on pg + +BEGIN; + +SELECT id, database_id, schema_id, private_schema_id, settings_table_id, api_name, private_api_name +FROM metaschema_modules_public.i18n_module +WHERE FALSE; + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/namespace_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/namespace_module/table.sql index 5dbc2e9a..8cfe5404 100644 --- a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/namespace_module/table.sql +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/namespace_module/table.sql @@ -3,9 +3,11 @@ BEGIN; SELECT id, database_id, schema_id, private_schema_id, + public_schema_name, private_schema_name, namespaces_table_id, namespace_events_table_id, namespaces_table_name, namespace_events_table_name, - membership_type, entity_table_id, policies + api_name, private_api_name, scope, prefix, + entity_table_id, policies, provisions, default_permissions FROM metaschema_modules_public.namespace_module WHERE false; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql new file mode 100644 index 00000000..940681c3 --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_credentials_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_credentials_module/table on pg + +BEGIN; + +SELECT verify_table('metaschema_modules_public.user_credentials_module'); + +ROLLBACK; diff --git a/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql new file mode 100644 index 00000000..7a56d8ee --- /dev/null +++ b/packages/metaschema-modules/verify/schemas/metaschema_modules_public/tables/user_settings_module/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_modules_public/tables/user_settings_module/table on pg + +BEGIN; + +SELECT verify_table('metaschema_modules_public.user_settings_module'); + +ROLLBACK; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/composite_type/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/composite_type/table.sql new file mode 100644 index 00000000..806328fc --- /dev/null +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/composite_type/table.sql @@ -0,0 +1,38 @@ +-- Deploy schemas/metaschema_public/tables/composite_type/table to pg + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/database/table +-- requires: schemas/metaschema_public/tables/schema/table +-- requires: schemas/metaschema_public/types/object_category + +BEGIN; + +CREATE TABLE metaschema_public.composite_type ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + + label text, + description text, + + attributes jsonb NOT NULL DEFAULT '[]', + + smart_tags jsonb, + + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + + tags citext[] NOT NULL DEFAULT '{}', + + CONSTRAINT db_fkey FOREIGN KEY (database_id) REFERENCES metaschema_public.database (id) ON DELETE CASCADE, + CONSTRAINT schema_fkey FOREIGN KEY (schema_id) REFERENCES metaschema_public.schema (id) ON DELETE CASCADE, + + UNIQUE (schema_id, name) +); + +CREATE INDEX composite_type_schema_id_idx ON metaschema_public.composite_type ( schema_id ); +CREATE INDEX composite_type_database_id_idx ON metaschema_public.composite_type ( database_id ); + +COMMIT; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql index a3b035e6..6182d432 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/indexes/databases_field_uniq_names_idx.sql @@ -10,7 +10,7 @@ CREATE UNIQUE INDEX databases_field_uniq_names_idx ON metaschema_public.field ( -- only apply normalization to uuid fields (FK candidates) to avoid false collisions on text fields like current_role/current_role_id table_id, DECODE(MD5(LOWER( CASE - WHEN type = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', '\1', 'i') + WHEN type->>'name' = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', '\1', 'i') ELSE name END )), 'hex') diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql index d0204b6b..27959dd0 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/field/table.sql @@ -36,13 +36,9 @@ CREATE TABLE metaschema_public.field ( is_required boolean NOT NULL DEFAULT FALSE, api_required boolean NOT NULL DEFAULT FALSE, - default_value text NULL DEFAULT NULL, - -- AST column for SQL expression validation (AST is the source of truth) - default_value_ast jsonb NULL DEFAULT NULL, + default_value jsonb NULL DEFAULT NULL, - type citext NOT NULL, - - -- typmods DO THIS SOON! + type jsonb NOT NULL, field_order int not null default 0, @@ -75,7 +71,4 @@ CREATE TABLE metaschema_public.field ( CREATE INDEX field_table_id_idx ON metaschema_public.field ( table_id ); CREATE INDEX field_database_id_idx ON metaschema_public.field ( database_id ); --- Smart comment for Graphile SQL expression validator plugin -COMMENT ON COLUMN metaschema_public.field.default_value IS E'@sqlExpression'; - COMMIT; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/full_text_search/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/full_text_search/table.sql index 892b488d..fb695602 100644 --- a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/full_text_search/table.sql +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/full_text_search/table.sql @@ -13,6 +13,7 @@ CREATE TABLE metaschema_public.full_text_search ( field_ids uuid[] NOT NULL, weights text[] NOT NULL, langs text[] NOT NULL, + lang_column text, created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now(), 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 new file mode 100644 index 00000000..05361345 --- /dev/null +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -0,0 +1,1883 @@ +-- Deploy schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed to pg +-- +-- GENERATED FILE — DO NOT EDIT +-- Regenerate with: cd packages/node-type-registry && pnpm generate +-- +-- Node types: 78 + +-- requires: schemas/metaschema_public/schema +-- requires: schemas/metaschema_public/tables/node_type_registry/table + +BEGIN; +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES ( + 'AuthzAllowAll', + 'authz_allow_all', + 'authz', + 'Public Access', + 'Allows all access. Generates TRUE expression.', + '{"type":"object","properties":{}}'::jsonb, + '{"authz"}'::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 ( + '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.', + '{"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":[]}'::jsonb, + '{"membership","authz"}'::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 ( + '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, + '{"composite","authz"}'::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 ( + 'AuthzDenyAll', + 'authz_deny_all', + 'authz', + 'No Access', + 'Denies all access. Generates FALSE expression.', + '{"type":"object","properties":{}}'::jsonb, + '{"authz"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"ownership","authz"}'::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 ( + '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, + '{"ownership","authz"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"membership","authz"}'::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 ( + '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.', + '{"type":"object","properties":{"shares_schema":{"type":"string","description":"Schema of the path_shares table"},"shares_table":{"type":"string","description":"Name of the path_shares table"},"files_schema":{"type":"string","description":"Schema of the files table (used to qualify column references inside the EXISTS subquery)"},"files_table":{"type":"string","description":"Name of the files table (used to qualify column references inside the EXISTS subquery)"},"permission_field":{"type":"string","format":"column-ref","description":"Boolean column on the path_shares table that grants the required permission (e.g. can_read, can_write)"},"bucket_field":{"type":"string","format":"column-ref","description":"Column on the files table referencing the bucket","default":"bucket_id"},"path_field":{"type":"string","format":"column-ref","description":"Ltree column on the files table representing the file path","default":"path"}},"required":["shares_schema","shares_table","files_table","permission_field"]}'::jsonb, + '{"storage","authz"}'::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 ( + '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, + '{"ownership","authz"}'::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 ( + 'AuthzMemberOwner', + 'authz_member_owner', + 'authz', + 'Member Owner', + 'Compound policy: the row must be owned by the current user (owner_field = current_user_id) AND the current user must be a member of the entity referenced by entity_field. Combines direct ownership with entity membership — the actor can only access rows they own within entities they belong to.', + '{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id)","default":"entity_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."},"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)"}},"required":["owner_field","entity_field"]}'::jsonb, + '{"ownership","membership","authz"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"membership","authz","restrictive"}'::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 ( + 'AuthzOrgHierarchy', + 'authz_org_hierarchy', + 'authz', + 'Org Hierarchy', + 'Organizational hierarchy visibility using closure table. Managers can see subordinate data or subordinates can see manager data.', + '{"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"]}'::jsonb, + '{"membership","hierarchy","authz"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"membership","peer","authz"}'::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 ( + 'AuthzPublishable', + 'authz_publishable', + 'authz', + 'Published Content', + 'Published state access control. Restricts access to records that are published.', + '{"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}}}'::jsonb, + '{"temporal","publishing","authz"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"membership","authz"}'::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 ( + '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, + '{"ownership","authz"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"membership","peer","authz"}'::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 ( + '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.', + '{"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"]}]}'::jsonb, + '{"temporal","authz"}'::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 ( + '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, + 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 ( + '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, + 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 ( + '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, + 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 ( + '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, + 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 ( + 'DataArchivable', + 'data_archivable', + 'data', + 'Archivable', + 'Adds user-reversible archive support with is_archived boolean and archived_at timestamp, plus a partial index for efficient active-row queries.', + '{"type":"object","properties":{"is_archived_field":{"type":"string","format":"column-ref","description":"Column name for the archive boolean flag","default":"is_archived"},"archived_at_field":{"type":"string","format":"column-ref","description":"Column name for the archive timestamp","default":"archived_at"},"include_id":{"type":"boolean","description":"If true, also adds a UUID primary key column with auto-generation","default":true}}}'::jsonb, + '{"schema"}'::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 ( + '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.', + '{"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, + 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 ( + '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[] +) 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 ( + '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, + '{"ownership","schema"}'::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 ( + '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, + '{"membership","schema"}'::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 ( + '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.', + '{"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, + 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 ( + 'DataI18n', + 'data_i18n', + 'data', + 'Internationalization', + 'Creates a companion _translations table with lang_code + translatable fields. Copies SELECT policies and column-ref fields from the base table. Adds @i18n smart comment so the Graphile i18n plugin discovers it. Requires i18n_module to be provisioned for the database.', + '{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names on the base table to make translatable. Each field is duplicated on the translation table with the same type."},"table_suffix":{"type":"string","description":"Suffix for the translation table name","default":"_translations"},"lang_code_type":{"type":"string","enum":["citext","text"],"description":"Type for the lang_code column","default":"citext"},"copy_mutation_policies":{"type":"boolean","description":"Whether to also copy INSERT/UPDATE/DELETE policies (not just SELECT). Default true — translations should be editable by the same users who can edit the base row.","default":true},"search":{"type":"object","description":"SearchFullText configuration for the translations table. When provided, creates a tsvector column on the translations table with lang_column=lang_code for dynamic per-row language stemming.","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column on the translations table","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the translatable source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"}},"required":["field"]},"description":"Translatable columns that feed the tsvector. Language is determined dynamically from the lang_code column of each row."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}},"required":["fields"]}'::jsonb, + '{"i18n","translation","schema"}'::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 ( + '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, + '{"primary_key","schema"}'::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 ( + '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.', + '{"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, + 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 ( + '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, + '{"transform","behavior"}'::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 ( + '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.', + '{"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, + 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 ( + '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":"object","description":"Default value as a FieldDefault object","default":{"value":{},"cast":{"name":"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, + 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 ( + 'DataMemberOwner', + 'data_member_owner', + 'data', + 'Member Owner', + 'Adds owner_id and entity_id columns with a compound AuthzMemberOwner policy. The actor must own the row (owner_id = current_user_id()) AND be a member of the entity (entity_id in SPRT). Use for private data within an entity scope — e.g., personal chat threads that belong to the company but only the author can see.', + '{"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},"membership_type":{"type":"integer","description":"Membership type for SPRT resolution. Required for entity-scoped provisioning.","default":null}}}'::jsonb, + '{"ownership","membership","security","schema"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"ownership","constraint","behavior"}'::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 ( + '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, + '{"ownership","membership","hierarchy","schema"}'::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 ( + '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, + '{"timestamps","schema"}'::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 ( + '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, + '{"publishing","temporal","schema"}'::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 ( + '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, + category, + display_name, + description, + 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.', + '{"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, + 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 ( + '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, + '{"schema"}'::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 ( + '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.', + '{"type":"object","properties":{"field_name":{"type":"string","format":"column-ref","description":"Column name for the status field","default":"status"},"type":{"type":"object","description":"Column type as a FieldType object","default":{"name":"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"}}}'::jsonb, + '{"status","schema"}'::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 ( + '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":"object","description":"Default value as a FieldDefault object","default":{"value":[],"cast":{"name":"citext","array_dimensions":1}}},"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, + 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 ( + '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, + '{"timestamps","schema"}'::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 ( + '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. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"max_depth":{"type":"integer","description":"Maximum depth to walk up the invite chain. Default 1 (direct inviter only). Set 2–10 to enable multi-level referral rewards. App-level only — must not be combined with entity_field.","default":1,"minimum":1,"maximum":10},"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. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["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. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"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. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["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","format":"function-ref","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe). Must match a registered function definition when function_module is installed."},"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)"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"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', + 'Enforce Aggregate 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\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"org\", \"data_room\", \"channel\", \"team\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"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","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 ( + '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","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","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"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for entity context resolution. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"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', + '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","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for per-entity cap lookups (only used for org scope). For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]}},"required":["feature_name"]}'::jsonb, + '{"limits","triggers","feature-flags","enforce","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 ( + 'LimitEnforceRate', + 'limit_enforce_rate', + 'limit_enforce', + 'Enforce Rate Limit', + 'Attaches a BEFORE trigger that calls check_rate_limit() to enforce sliding-window rate limits before allowing mutations. The function checks all three scopes (entity, actor-in-entity, actor) in a single call; which scopes are actually enforced is controlled by what rows exist in rate_window_limits (plan-based config). Requires a provisioned meter_rate_limits_module and billing_module for the target database.', + '{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry, e.g. \"messaging\", \"inference\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limiting. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id (user) for rate limiting","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Which DML events to enforce rate limits on (DELETE is excluded since it reduces load)","default":["INSERT"]}},"required":["meter_slug"]}'::jsonb, + '{"rate-limits","triggers","enforce","metering","abuse-protection"}'::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 ( + '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 (or references) the entity id for billing. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"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, + 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 ( + 'LimitWarningAggregate', + 'limit_warning_aggregate', + 'limit_warning', + 'Warning Aggregate', + 'Attaches an AFTER INSERT trigger that checks if the entity''s aggregate usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor/entity triple. Requires a provisioned limits_module with limit_warnings and aggregate limits enabled.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to watch (must match a limit_warnings.name entry, e.g. \"databases\", \"members\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"org\", \"data_room\", \"channel\", \"team\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","aggregates","warning","notifications"}'::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 ( + 'LimitWarningCounter', + 'limit_warning_counter', + 'limit_warning', + 'Warning Counter', + 'Attaches an AFTER INSERT trigger that checks if the actor''s current usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires a provisioned limits_module with limit_warnings enabled.', + '{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to watch (must match a limit_warnings.name entry, e.g. \"projects\", \"members\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for limit lookup","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id. When provided, entity_id is included in the job payload and dedup state. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}'::jsonb, + '{"limits","triggers","warning","notifications"}'::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 ( + 'LimitWarningRate', + 'limit_warning_rate', + 'limit_warning', + 'Warning Rate Limit', + 'Attaches an AFTER INSERT trigger that checks if the actor''s current request count in the active sliding window has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires both a limits_module with limit_warnings enabled and a rate_limit_meters_module.', + '{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry)"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use for warnings and warning_state tables. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \"app\", \"org\", \"data_room\", \"channel\", \"team\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \"channels\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \"public\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \"entity_id\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for rate limit lookup","default":"owner_id"}},"required":["meter_slug"]}'::jsonb, + '{"rate-limits","triggers","warning","notifications","metering"}'::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"},"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."},"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"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override (e.g. [\"fulltext\", \"bm25\"])."},"entity_field":{"type":"string","format":"column-ref","description":"Column on the parent table that holds (or references) the entity_id for billing scope. Forwarded to the chunking job trigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the chunking job trigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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[] +) 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","$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":{"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"},"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."},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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}}}'::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","$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":{"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":{}},"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."},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override."},"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, + '{"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","$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":{"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":{}},"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."},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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"}}}}}'::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","$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."}}}},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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, + category, + display_name, + description, + 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.', + '{"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"]}'::jsonb, + '{"relation","foreign_key","schema"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"relation","foreign_key","has_many","schema"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"relation","foreign_key","unique","schema"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"relation","junction","many_to_many","schema"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"relation","spatial","postgis","schema"}'::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 ( + '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.', + '{"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"]}'::jsonb, + '{"search","bm25","schema"}'::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 ( + '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.', + '{"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."},"lang_column":{"type":"string","format":"column-ref","description":"Column name whose value determines the text search configuration per row. When set, the tsvector trigger uses NEW.::regconfig instead of a static language, enabling dynamic per-row language stemming. The per-field lang values in source_fields are used as fallback defaults for the langs array but the trigger reads from this column at runtime."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}'::jsonb, + '{"search","fts","tsvector","schema"}'::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).', + '{"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"}}}'::jsonb, + '{"spatial","postgis","geometry","schema"}'::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.', + '{"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"]}'::jsonb, + '{"spatial","postgis","geometry","aggregate","schema"}'::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.', + '{"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"]}'::jsonb, + '{"search","trigram","schema"}'::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.', + '{"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"}},"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."},"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_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, + 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.', + '{"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."},"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."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","format":"function-ref","description":"Task identifier for the job queue. Must match a registered function definition when function_module is installed.","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","format":"function-ref","description":"Task identifier for the chunking job queue. Must match a registered function definition when function_module is installed.","default":"generate_chunks"}}}}}'::jsonb, + '{"embedding","vector","ai","schema"}'::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.', + '{"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"]}'::jsonb, + '{"view","aggregate","reporting"}'::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.).', + '{"type":"object","properties":{"query_ast":{"type":"object","description":"Composite SELECT query AST (JSONB)"}},"required":["query_ast"]}'::jsonb, + '{"view","advanced","composite","ast"}'::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.', + '{"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"]}'::jsonb, + '{"view","filter","authz"}'::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.', + '{"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"]}'::jsonb, + '{"view","join"}'::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.', + '{"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"]}'::jsonb, + '{"view","projection"}'::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; +COMMIT; diff --git a/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/table.sql b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/table.sql new file mode 100644 index 00000000..e2cd74e5 --- /dev/null +++ b/packages/metaschema-schema/deploy/schemas/metaschema_public/tables/node_type_registry/table.sql @@ -0,0 +1,20 @@ +-- Deploy schemas/metaschema_public/tables/node_type_registry/table to pg + +-- requires: schemas/metaschema_public/schema + +BEGIN; + +CREATE TABLE metaschema_public.node_type_registry ( + name text PRIMARY KEY, + slug text NOT NULL UNIQUE, + category text NOT NULL, + display_name text, + description text, + parameter_schema jsonb NOT NULL DEFAULT '{}'::jsonb, + tags text[] NOT NULL DEFAULT '{}'::text[] +); + +CREATE INDEX node_type_registry_category_idx + ON metaschema_public.node_type_registry (category); + +COMMIT; diff --git a/packages/metaschema-schema/pgpm.plan b/packages/metaschema-schema/pgpm.plan index 11b59439..3f6c02ea 100644 --- a/packages/metaschema-schema/pgpm.plan +++ b/packages/metaschema-schema/pgpm.plan @@ -32,6 +32,8 @@ schemas/metaschema_public/tables/enum/table [schemas/metaschema_public/schema sc schemas/metaschema_public/tables/embedding_chunks/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table] 2026-03-19T00:00:00Z devin # add schemas/metaschema_public/tables/embedding_chunks/table schemas/metaschema_public/tables/spatial_relation/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table schemas/metaschema_public/types/object_category] 2026-04-17T00:00:00Z devin # add schemas/metaschema_public/tables/spatial_relation/table - +schemas/metaschema_public/tables/node_type_registry/table [schemas/metaschema_public/schema] 2026-04-30T00:00:00Z Constructive # add schemas/metaschema_public/tables/node_type_registry/table +schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed [schemas/metaschema_public/schema schemas/metaschema_public/tables/node_type_registry/table] 2026-04-30T00:00:01Z Constructive # seed node_type_registry data from upstream TS registry schemas/metaschema_public/tables/function/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table] 2026-05-09T00:00:00Z devin # add metaschema_public.function table for tracking generated SQL functions schemas/metaschema_public/tables/partition/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/table/table schemas/metaschema_public/tables/field/table] 2026-05-26T00:00:00Z Constructive # add metaschema_public.partition table for pg_partman lifecycle config +schemas/metaschema_public/tables/composite_type/table [schemas/metaschema_public/schema schemas/metaschema_public/tables/database/table schemas/metaschema_public/tables/schema/table schemas/metaschema_public/types/object_category] 2026-05-29T00:00:00Z devin # add metaschema_public.composite_type table for generated composite types diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/composite_type/table.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/composite_type/table.sql new file mode 100644 index 00000000..bc31bc06 --- /dev/null +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/composite_type/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/composite_type/table from pg + +BEGIN; + +DROP TABLE metaschema_public.composite_type; + +COMMIT; diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql new file mode 100644 index 00000000..1fd02531 --- /dev/null +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -0,0 +1,10 @@ +-- Revert schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed from pg +-- +-- GENERATED FILE — DO NOT EDIT +-- Regenerate with: cd packages/node-type-registry && pnpm generate + +BEGIN; + +DELETE FROM metaschema_public.node_type_registry; + +COMMIT; diff --git a/packages/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/table.sql b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/table.sql new file mode 100644 index 00000000..7e0f75fe --- /dev/null +++ b/packages/metaschema-schema/revert/schemas/metaschema_public/tables/node_type_registry/table.sql @@ -0,0 +1,7 @@ +-- Revert schemas/metaschema_public/tables/node_type_registry/table from pg + +BEGIN; + +DROP TABLE IF EXISTS metaschema_public.node_type_registry; + +COMMIT; diff --git a/packages/metaschema-schema/sql/metaschema-schema--0.26.3.sql b/packages/metaschema-schema/sql/metaschema-schema--0.26.3.sql index ef2e354a..e20b0a3e 100644 --- a/packages/metaschema-schema/sql/metaschema-schema--0.26.3.sql +++ b/packages/metaschema-schema/sql/metaschema-schema--0.26.3.sql @@ -165,9 +165,8 @@ CREATE TABLE metaschema_public.field ( smart_tags jsonb, is_required boolean NOT NULL DEFAULT false, api_required boolean NOT NULL DEFAULT false, - default_value text NULL DEFAULT NULL, - default_value_ast jsonb NULL DEFAULT NULL, - type citext NOT NULL, + default_value jsonb NULL DEFAULT NULL, + type jsonb NOT NULL, field_order int NOT NULL DEFAULT 0, regexp text DEFAULT NULL, chk jsonb DEFAULT NULL, @@ -195,10 +194,8 @@ CREATE INDEX field_table_id_idx ON metaschema_public.field (table_id); CREATE INDEX field_database_id_idx ON metaschema_public.field (database_id); -COMMENT ON COLUMN metaschema_public.field.default_value IS '@sqlExpression'; - CREATE UNIQUE INDEX databases_field_uniq_names_idx ON metaschema_public.field (table_id, (decode(md5(lower(CASE - WHEN type = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', E'\\1', 'i') + WHEN (type ->> 'name') = 'uuid' THEN regexp_replace(name, '^(.+?)(_row_id|_id|_uuid|_fk|_pk)$', E'\\1', 'i') ELSE name END)), 'hex'))); @@ -247,6 +244,7 @@ CREATE TABLE metaschema_public.full_text_search ( field_ids uuid[] NOT NULL, weights text[] NOT NULL, langs text[] NOT NULL, + lang_column text, created_at timestamptz DEFAULT now(), updated_at timestamptz DEFAULT now(), CONSTRAINT db_fkey @@ -776,6 +774,876 @@ CREATE INDEX spatial_relation_ref_table_id_idx ON metaschema_public.spatial_rela CREATE INDEX spatial_relation_ref_field_id_idx ON metaschema_public.spatial_relation (ref_field_id); +CREATE TABLE metaschema_public.node_type_registry ( + name text PRIMARY KEY, + slug text NOT NULL UNIQUE, + category text NOT NULL, + display_name text, + description text, + parameter_schema jsonb NOT NULL DEFAULT '{}'::jsonb, + tags text[] NOT NULL DEFAULT CAST('{}' AS text[]) +); + +CREATE INDEX node_type_registry_category_idx ON metaschema_public.node_type_registry (category); + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('AuthzMemberOwner', 'authz_member_owner', 'authz', 'Member Owner', 'Compound policy: the row must be owned by the current user (owner_field = current_user_id) AND the current user must be a member of the entity referenced by entity_field. Combines direct ownership with entity membership — the actor can only access rows they own within entities they belong to.', CAST('{"type":"object","properties":{"owner_field":{"type":"string","format":"column-ref","description":"Column name containing the owner user ID (e.g., owner_id)","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column name referencing the entity (e.g., entity_id)","default":"entity_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."},"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)"}},"required":["owner_field","entity_field"]}' AS jsonb), CAST('{"ownership","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, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataArchivable', 'data_archivable', 'data', 'Archivable', 'Adds user-reversible archive support with is_archived boolean and archived_at timestamp, plus a partial index for efficient active-row queries.', '{"type":"object","properties":{"is_archived_field":{"type":"string","format":"column-ref","description":"Column name for the archive boolean flag","default":"is_archived"},"archived_at_field":{"type":"string","format":"column-ref","description":"Column name for the archive timestamp","default":"archived_at"},"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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('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, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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},"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, + slug, + category, + display_name, + description, + 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},"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, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataI18n', 'data_i18n', 'data', 'Internationalization', 'Creates a companion _translations table with lang_code + translatable fields. Copies SELECT policies and column-ref fields from the base table. Adds @i18n smart comment so the Graphile i18n plugin discovers it. Requires i18n_module to be provisioned for the database.', CAST('{"type":"object","properties":{"fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"Field names on the base table to make translatable. Each field is duplicated on the translation table with the same type."},"table_suffix":{"type":"string","description":"Suffix for the translation table name","default":"_translations"},"lang_code_type":{"type":"string","enum":["citext","text"],"description":"Type for the lang_code column","default":"citext"},"copy_mutation_policies":{"type":"boolean","description":"Whether to also copy INSERT/UPDATE/DELETE policies (not just SELECT). Default true — translations should be editable by the same users who can edit the base row.","default":true},"search":{"type":"object","description":"SearchFullText configuration for the translations table. When provided, creates a tsvector column on the translations table with lang_column=lang_code for dynamic per-row language stemming.","properties":{"field_name":{"type":"string","format":"column-ref","description":"Name of the tsvector column on the translations table","default":"search"},"source_fields":{"type":"array","items":{"type":"object","properties":{"field":{"type":"string","format":"column-ref","description":"Name of the translatable source column"},"weight":{"type":"string","enum":["A","B","C","D"],"description":"tsvector weight class (A=highest, D=lowest)","default":"D"}},"required":["field"]},"description":"Translatable columns that feed the tsvector. Language is determined dynamically from the lang_code column of each row."},"search_score_weight":{"type":"number","description":"Weight for this algorithm in composite searchScore","default":1}},"required":["source_fields"]}},"required":["fields"]}' AS jsonb), CAST('{"i18n","translation","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 + ('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, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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":"object","description":"Default value as a FieldDefault object","default":{"value":{},"cast":{"name":"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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('DataMemberOwner', 'data_member_owner', 'data', 'Member Owner', 'Adds owner_id and entity_id columns with a compound AuthzMemberOwner policy. The actor must own the row (owner_id = current_user_id()) AND be a member of the entity (entity_id in SPRT). Use for private data within an entity scope — e.g., personal chat threads that belong to the company but only the author can see.', '{"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},"membership_type":{"type":"integer","description":"Membership type for SPRT resolution. Required for entity-scoped provisioning.","default":null}}}'::jsonb, CAST('{"ownership","membership","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, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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},"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, + slug, + category, + display_name, + description, + 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},"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, + slug, + category, + display_name, + description, + 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_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, + 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.', 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, + slug, + category, + display_name, + description, + 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","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, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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":"object","description":"Column type as a FieldType object","default":{"name":"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, + slug, + category, + display_name, + description, + 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":"object","description":"Default value as a FieldDefault object","default":{"value":[],"cast":{"name":"citext","array_dimensions":1}}},"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, + slug, + category, + display_name, + description, + 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; + +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.', 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. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"max_depth":{"type":"integer","description":"Maximum depth to walk up the invite chain. Default 1 (direct inviter only). Set 2–10 to enable multi-level referral rewards. App-level only — must not be combined with entity_field.","default":1,"minimum":1,"maximum":10},"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. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["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, + 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.', 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. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup. Omit for user-only events."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"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. Accepts a single leaf condition, an array of conditions (implicitly AND), or a nested combinator tree ({AND: [...], OR: [...], NOT: {...}}). Each leaf is {field, op, value?, row?, ref?}. Column types are resolved automatically from the table schema. Cannot be combined with condition_field or watch_fields.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"watch_fields":{"type":"array","items":{"type":"string","format":"column-ref"},"description":"For UPDATE triggers, only fire when these fields change (uses DISTINCT FROM)"}},"required":["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, + 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).', 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","format":"function-ref","description":"Job task identifier passed to add_job (e.g., process_invoice, sync_to_stripe). Must match a registered function definition when function_module is installed."},"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)"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitEnforceAggregate', 'limit_enforce_aggregate', 'limit_enforce', 'Enforce Aggregate 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\\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"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","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, + 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.', 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","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","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"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for entity context resolution. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitEnforceFeature', 'limit_enforce_feature', 'limit_enforce', '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","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for per-entity cap lookups (only used for org scope). For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]}},"required":["feature_name"]}' AS jsonb), CAST('{"limits","triggers","feature-flags","enforce","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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitEnforceRate', 'limit_enforce_rate', 'limit_enforce', 'Enforce Rate Limit', 'Attaches a BEFORE trigger that calls check_rate_limit() to enforce sliding-window rate limits before allowing mutations. The function checks all three scopes (entity, actor-in-entity, actor) in a single call; which scopes are actually enforced is controlled by what rows exist in rate_window_limits (plan-based config). Requires a provisioned meter_rate_limits_module and billing_module for the target database.', CAST(E'{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry, e.g. \\"messaging\\", \\"inference\\")"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limiting. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id (user) for rate limiting","default":"owner_id"},"events":{"type":"array","items":{"type":"string","enum":["INSERT","UPDATE"]},"description":"Which DML events to enforce rate limits on (DELETE is excluded since it reduces load)","default":["INSERT"]}},"required":["meter_slug"]}' AS jsonb), CAST('{"rate-limits","triggers","enforce","metering","abuse-protection"}' 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 + ('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 (or references) the entity id for billing. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"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, + slug, + category, + display_name, + description, + parameter_schema, + tags +) VALUES + ('LimitWarningAggregate', 'limit_warning_aggregate', 'limit_warning', 'Warning Aggregate', 'Attaches an AFTER INSERT trigger that checks if the entity''s aggregate usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor/entity triple. Requires a provisioned limits_module with limit_warnings and aggregate limits enabled.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the aggregate limit to watch (must match a limit_warnings.name entry, e.g. \\"databases\\", \\"members\\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"org"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for aggregate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","aggregates","warning","notifications"}' 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 + ('LimitWarningCounter', 'limit_warning_counter', 'limit_warning', 'Warning Counter', 'Attaches an AFTER INSERT trigger that checks if the actor''s current usage has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires a provisioned limits_module with limit_warnings enabled.', CAST(E'{"type":"object","properties":{"limit_name":{"type":"string","description":"Name of the limit to watch (must match a limit_warnings.name entry, e.g. \\"projects\\", \\"members\\")"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"app"},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for limit lookup","default":"owner_id"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id. When provided, entity_id is included in the job payload and dedup state. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]}},"required":["limit_name"]}' AS jsonb), CAST('{"limits","triggers","warning","notifications"}' 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 + ('LimitWarningRate', 'limit_warning_rate', 'limit_warning', 'Warning Rate Limit', 'Attaches an AFTER INSERT trigger that checks if the actor''s current request count in the active sliding window has crossed any warning threshold configured in the limit_warnings table. If a threshold is reached for the first time, enqueues a background job (e.g. email notification). Uses limit_warning_state for one-time dedup per warning/actor pair. Requires both a limits_module with limit_warnings enabled and a rate_limit_meters_module.', CAST(E'{"type":"object","properties":{"meter_slug":{"type":"string","description":"Slug of the billing meter to check rate limits against (must match a meters table entry)"},"scope":{"type":"string","description":"Membership type prefix that determines which limits_module row to use for warnings and warning_state tables. Resolved dynamically via memberships_module — supports any provisioned type (e.g. \\"app\\", \\"org\\", \\"data_room\\", \\"channel\\", \\"team\\").","default":"app"},"entity_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds (or references) the entity id for rate limit lookup. For direct entity_id columns, just set this field. For FK lookups (e.g., channel_id → channels.entity_id), combine with entity_lookup.","default":"entity_id"},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Used when entity_field is a FK (e.g., channel_id) rather than a direct entity_id. The generator validates all fields against metaschema within the same database_id.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from (e.g., \\"channels\\"). Required."},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, e.g., \\"public\\"). Optional — if omitted, resolved by table name within the same database_id (raises error if ambiguous)."},"obj_field":{"type":"string","description":"Column on the related table that holds the entity_id (e.g., \\"entity_id\\"). Required."}},"required":["obj_table","obj_field"]},"actor_field":{"type":"string","format":"column-ref","description":"Column on the target table that holds the actor id for rate limit lookup","default":"owner_id"}},"required":["meter_slug"]}' AS jsonb), CAST('{"rate-limits","triggers","warning","notifications","metering"}' 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 + ('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"},"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."},"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"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override (e.g. [\\"fulltext\\", \\"bm25\\"])."},"entity_field":{"type":"string","format":"column-ref","description":"Column on the parent table that holds (or references) the entity_id for billing scope. Forwarded to the chunking job trigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the chunking job trigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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"}}}' 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, + 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.', 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":{"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"},"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."},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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}}}' 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, + 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.', 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":{"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":{}},"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."},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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"},"search_indexes":{"type":"array","items":{"type":"string","enum":["fulltext","bm25","trigram"]},"description":"Text search indexes to create on the chunks content column. Omit to mirror the parent table''s text search indexes. Set explicitly to override."},"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","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, + 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.', 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":{"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":{}},"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."},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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"}}}}}' 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, + 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.', 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."}}}},"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 auto-generated filtering. Merged with the auto-generated conditions via AND.","x-codegen-type":"TriggerCondition | TriggerCondition[]","oneOf":[{"$ref":"#/$defs/triggerCondition"},{"type":"array","items":{"$ref":"#/$defs/triggerCondition"}}]},"entity_field":{"type":"string","format":"column-ref","description":"Column on the trigger table that holds (or references) the entity_id for billing scope. Forwarded to the composed JobTrigger."},"entity_lookup":{"type":"object","description":"FK lookup configuration for resolving entity_id through a related table. Forwarded to the composed JobTrigger.","properties":{"obj_table":{"type":"string","description":"Name of the related table to look up entity_id from"},"obj_schema":{"type":"string","description":"Schema of the related table (user-facing name, optional)"},"obj_field":{"type":"string","format":"column-ref","description":"Column on the related table that holds the entity_id"}},"required":["obj_table","obj_field"]},"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, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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; + +INSERT INTO metaschema_public.node_type_registry ( + name, + slug, + category, + display_name, + description, + 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."},"lang_column":{"type":"string","format":"column-ref","description":"Column name whose value determines the text search configuration per row. When set, the tsvector trigger uses NEW.::regconfig instead of a static language, enabling dynamic per-row language stemming. The per-field lang values in source_fields are used as fallback defaults for the langs array but the trigger reads from this column at runtime."},"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"}},"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."},"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_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."},"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."},"enqueue_job":{"type":"boolean","description":"Auto-create trigger that enqueues embedding generation jobs","default":true},"job_task_name":{"type":"string","format":"function-ref","description":"Task identifier for the job queue. Must match a registered function definition when function_module is installed.","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","format":"function-ref","description":"Task identifier for the chunking job queue. Must match a registered function definition when function_module is installed.","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; + +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, @@ -825,4 +1693,32 @@ CREATE TABLE metaschema_public.partition ( UNIQUE (table_id) ); -CREATE INDEX partition_database_id_idx ON metaschema_public.partition (database_id); \ No newline at end of file +CREATE INDEX partition_database_id_idx ON metaschema_public.partition (database_id); + +CREATE TABLE metaschema_public.composite_type ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + name text NOT NULL, + label text, + description text, + attributes jsonb NOT NULL DEFAULT '[]', + smart_tags jsonb, + category metaschema_public.object_category NOT NULL DEFAULT 'app', + module text NULL, + scope int NULL, + tags citext[] NOT NULL DEFAULT '{}', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT schema_fkey + FOREIGN KEY(schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE CASCADE, + UNIQUE (schema_id, name) +); + +CREATE INDEX composite_type_schema_id_idx ON metaschema_public.composite_type (schema_id); + +CREATE INDEX composite_type_database_id_idx ON metaschema_public.composite_type (database_id); \ No newline at end of file diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql new file mode 100644 index 00000000..de512a0c --- /dev/null +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/composite_type/table.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/composite_type/table on pg + +BEGIN; + +SELECT verify_table ('metaschema_public.composite_type'); + +ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql new file mode 100644 index 00000000..9eabe731 --- /dev/null +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed.sql @@ -0,0 +1,7 @@ +-- Verify schemas/metaschema_public/tables/node_type_registry/fixtures/node_type_registry_seed on pg + +BEGIN; + +SELECT 1 FROM metaschema_public.node_type_registry WHERE name = 'AuthzDirectOwner'; + +ROLLBACK; diff --git a/packages/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/table.sql b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/table.sql new file mode 100644 index 00000000..bd2cacab --- /dev/null +++ b/packages/metaschema-schema/verify/schemas/metaschema_public/tables/node_type_registry/table.sql @@ -0,0 +1,9 @@ +-- Verify schemas/metaschema_public/tables/node_type_registry/table on pg + +BEGIN; + +SELECT name, slug, category, display_name, description, parameter_schema, tags +FROM metaschema_public.node_type_registry +WHERE FALSE; + +ROLLBACK; diff --git a/packages/object-tree/Makefile b/packages/object-tree/Makefile index 8cf8de6e..6c1e3f11 100644 --- a/packages/object-tree/Makefile +++ b/packages/object-tree/Makefile @@ -1,5 +1,5 @@ EXTENSION = object-tree -DATA = sql/object-tree--0.26.1.sql +DATA = sql/object-tree--0.26.2.sql PG_CONFIG = pg_config PGXS := $(shell $(PG_CONFIG) --pgxs) diff --git a/packages/object-tree/object-tree.control b/packages/object-tree/object-tree.control index 0d18ea0f..116883bd 100644 --- a/packages/object-tree/object-tree.control +++ b/packages/object-tree/object-tree.control @@ -1,6 +1,6 @@ # object-tree extension comment = 'object-tree extension - git-like version control for database objects' -default_version = '0.26.1' +default_version = '0.26.2' module_pathname = '$libdir/object-tree' requires = 'plpgsql,pgcrypto,object-store,pgpm-verify' relocatable = false diff --git a/packages/object-tree/package.json b/packages/object-tree/package.json index 3914b420..82d7c244 100644 --- a/packages/object-tree/package.json +++ b/packages/object-tree/package.json @@ -38,4 +38,4 @@ "bugs": { "url": "https://github.com/constructive-io/pgpm-modules/issues" } -} +} \ No newline at end of file diff --git a/packages/object-tree/sql/object-tree--0.26.1.sql b/packages/object-tree/sql/object-tree--0.26.2.sql similarity index 100% rename from packages/object-tree/sql/object-tree--0.26.1.sql rename to packages/object-tree/sql/object-tree--0.26.2.sql diff --git a/packages/services/Makefile b/packages/services/Makefile new file mode 100644 index 00000000..d5cd6eeb --- /dev/null +++ b/packages/services/Makefile @@ -0,0 +1,6 @@ +EXTENSION = services +DATA = sql/services--0.26.3.sql + +PG_CONFIG = pg_config +PGXS := $(shell $(PG_CONFIG) --pgxs) +include $(PGXS) 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 58dbe5e7..71d7e12f 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 @@ -24,6 +24,7 @@ CREATE TABLE services_public.api_settings ( enable_llm boolean, enable_realtime boolean, enable_bulk boolean, + enable_i18n boolean, -- Extensible JSON for future settings that don't warrant their own column options jsonb NOT NULL DEFAULT '{}'::jsonb, @@ -49,6 +50,7 @@ COMMENT ON COLUMN services_public.api_settings.enable_ltree IS 'Override: enable 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.enable_i18n IS 'Override: enable internationalization plugin (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 a8ab600c..ec660def 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 @@ -21,6 +21,7 @@ CREATE TABLE services_public.database_settings ( enable_llm boolean NOT NULL DEFAULT false, enable_realtime boolean NOT NULL DEFAULT false, enable_bulk boolean NOT NULL DEFAULT false, + enable_i18n boolean NOT NULL DEFAULT false, -- Extensible JSON for future settings that don't warrant their own column options jsonb NOT NULL DEFAULT '{}'::jsonb, @@ -44,6 +45,7 @@ COMMENT ON COLUMN services_public.database_settings.enable_ltree IS 'Enable ltre 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.enable_i18n IS 'Enable internationalization plugin (localeStrings field, translation table discovery) 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 1af3bc4b..8b21c28c 100644 --- a/packages/services/package.json +++ b/packages/services/package.json @@ -35,4 +35,4 @@ "bugs": { "url": "https://github.com/constructive-io/constructive-db/issues" } -} +} \ No newline at end of file diff --git a/packages/services/services.control b/packages/services/services.control index d08f57ab..8f6e433d 100644 --- a/packages/services/services.control +++ b/packages/services/services.control @@ -1,6 +1,6 @@ # services extension comment = 'services extension - schemas for module registration and service configuration' -default_version = '0.1.0' +default_version = '0.26.3' module_pathname = '$libdir/services' requires = 'plpgsql,metaschema-schema,pgpm-verify' relocatable = false diff --git a/packages/services/sql/services--0.26.3.sql b/packages/services/sql/services--0.26.3.sql new file mode 100644 index 00000000..b1d54b02 --- /dev/null +++ b/packages/services/sql/services--0.26.3.sql @@ -0,0 +1,838 @@ +\echo Use "CREATE EXTENSION services" to load this file. \quit +CREATE SCHEMA services_private; + +GRANT USAGE ON SCHEMA services_private TO authenticated; + +GRANT USAGE ON SCHEMA services_private TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private + GRANT ALL ON TABLES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private + GRANT ALL ON SEQUENCES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_private + GRANT ALL ON FUNCTIONS TO administrator; + +CREATE SCHEMA services_public; + +GRANT USAGE ON SCHEMA services_public TO authenticated; + +GRANT USAGE ON SCHEMA services_public TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public + GRANT ALL ON TABLES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public + GRANT ALL ON SEQUENCES TO administrator; + +ALTER DEFAULT PRIVILEGES IN SCHEMA services_public + GRANT ALL ON FUNCTIONS TO administrator; + +CREATE TABLE services_public.apis ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + name text NOT NULL, + dbname text NOT NULL DEFAULT current_database(), + role_name text NOT NULL DEFAULT 'authenticated', + anon_role text NOT NULL DEFAULT 'anonymous', + is_public boolean NOT NULL DEFAULT true, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + UNIQUE (database_id, name) +); + +COMMENT ON TABLE services_public.apis IS 'API endpoint configurations: each record defines a PostGraphile/PostgREST API with its database role and public access settings'; + +COMMENT ON COLUMN services_public.apis.id IS 'Unique identifier for this API'; + +COMMENT ON COLUMN services_public.apis.database_id IS 'Reference to the metaschema database this API serves'; + +COMMENT ON COLUMN services_public.apis.name IS 'Unique name for this API within its database'; + +COMMENT ON COLUMN services_public.apis.dbname IS 'PostgreSQL database name to connect to'; + +COMMENT ON COLUMN services_public.apis.role_name IS 'PostgreSQL role used for authenticated requests'; + +COMMENT ON COLUMN services_public.apis.anon_role IS 'PostgreSQL role used for anonymous/unauthenticated requests'; + +COMMENT ON COLUMN services_public.apis.is_public IS 'Whether this API is publicly accessible without authentication'; + +CREATE INDEX apis_database_id_idx ON services_public.apis (database_id); + +CREATE TABLE services_public.api_modules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid NOT NULL, + name text NOT NULL, + data pg_catalog.json NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.api_modules IS 'Server-side module configuration for an API endpoint; stores module name and JSON settings used by the application server'; + +COMMENT ON COLUMN services_public.api_modules.id IS 'Unique identifier for this API module record'; + +COMMENT ON COLUMN services_public.api_modules.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.api_modules.api_id IS 'API this module configuration belongs to'; + +COMMENT ON COLUMN services_public.api_modules.name IS 'Module name (e.g. auth, uploads, webhooks)'; + +COMMENT ON COLUMN services_public.api_modules.data IS 'JSON configuration data for this module'; + +ALTER TABLE services_public.api_modules + ADD CONSTRAINT api_modules_api_id_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id); + +CREATE INDEX api_modules_api_id_idx ON services_public.api_modules (api_id); + +CREATE INDEX api_modules_database_id_idx ON services_public.api_modules (database_id); + +CREATE TABLE services_public.api_schemas ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + schema_id uuid NOT NULL, + api_id uuid 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, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE, + UNIQUE (api_id, schema_id) +); + +COMMENT ON TABLE services_public.api_schemas IS 'Join table linking APIs to the database schemas they expose; controls which schemas are accessible through each API'; + +COMMENT ON COLUMN services_public.api_schemas.id IS 'Unique identifier for this API-schema mapping'; + +COMMENT ON COLUMN services_public.api_schemas.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.api_schemas.schema_id IS 'Metaschema schema being exposed through the API'; + +COMMENT ON COLUMN services_public.api_schemas.api_id IS 'API that exposes this schema'; + +CREATE INDEX api_schemas_database_id_idx ON services_public.api_schemas (database_id); + +CREATE INDEX api_schemas_schema_id_idx ON services_public.api_schemas (schema_id); + +CREATE INDEX api_schemas_api_id_idx ON services_public.api_schemas (api_id); + +CREATE TABLE services_public.sites ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + title text, + description text, + og_image image, + favicon attachment, + apple_touch_icon image, + logo image, + dbname text NOT NULL DEFAULT current_database(), + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT max_title + CHECK (character_length(title) <= 120), + CONSTRAINT max_descr + CHECK (character_length(description) <= 120) +); + +COMMENT ON TABLE services_public.sites IS 'Top-level site configuration: branding assets, title, and description for a deployed application'; + +COMMENT ON COLUMN services_public.sites.id IS 'Unique identifier for this site'; + +COMMENT ON COLUMN services_public.sites.database_id IS 'Reference to the metaschema database this site belongs to'; + +COMMENT ON COLUMN services_public.sites.title IS 'Display title for the site (max 120 characters)'; + +COMMENT ON COLUMN services_public.sites.description IS 'Short description of the site (max 120 characters)'; + +COMMENT ON COLUMN services_public.sites.og_image IS 'Open Graph image used for social media link previews'; + +COMMENT ON COLUMN services_public.sites.favicon IS 'Browser favicon attachment'; + +COMMENT ON COLUMN services_public.sites.apple_touch_icon IS 'Apple touch icon for iOS home screen bookmarks'; + +COMMENT ON COLUMN services_public.sites.logo IS 'Primary logo image for the site'; + +COMMENT ON COLUMN services_public.sites.dbname IS 'PostgreSQL database name this site connects to'; + +CREATE INDEX sites_database_id_idx ON services_public.sites (database_id); + +CREATE TABLE services_public.apps ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + name text, + app_image image, + app_store_link url, + app_store_id text, + app_id_prefix text, + play_store_link url, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + UNIQUE (site_id) +); + +COMMENT ON TABLE services_public.apps IS 'Mobile and native app configuration linked to a site, including store links and identifiers'; + +COMMENT ON COLUMN services_public.apps.id IS 'Unique identifier for this app'; + +COMMENT ON COLUMN services_public.apps.database_id IS 'Reference to the metaschema database this app belongs to'; + +COMMENT ON COLUMN services_public.apps.site_id IS 'Site this app is associated with (one app per site)'; + +COMMENT ON COLUMN services_public.apps.name IS 'Display name of the app'; + +COMMENT ON COLUMN services_public.apps.app_image IS 'App icon or promotional image'; + +COMMENT ON COLUMN services_public.apps.app_store_link IS 'URL to the Apple App Store listing'; + +COMMENT ON COLUMN services_public.apps.app_store_id IS 'Apple App Store application identifier'; + +COMMENT ON COLUMN services_public.apps.app_id_prefix IS 'Apple App ID prefix (Team ID) for universal links and associated domains'; + +COMMENT ON COLUMN services_public.apps.play_store_link IS 'URL to the Google Play Store listing'; + +ALTER TABLE services_public.apps + ADD CONSTRAINT apps_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX apps_site_id_idx ON services_public.apps (site_id); + +CREATE INDEX apps_database_id_idx ON services_public.apps (database_id); + +CREATE TABLE services_public.domains ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid, + site_id uuid, + subdomain hostname, + domain hostname, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE, + CONSTRAINT site_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id) + ON DELETE CASCADE, + CONSTRAINT one_route_chk + CHECK ( + (api_id IS NULL + AND site_id IS NULL) + OR (api_id IS NULL + AND site_id IS NOT NULL) + OR (api_id IS NOT NULL + AND site_id IS NULL) + ), + UNIQUE (subdomain, domain) +); + +COMMENT ON TABLE services_public.domains IS 'DNS domain and subdomain routing: maps hostnames to either an API endpoint or a site'; + +COMMENT ON COLUMN services_public.domains.id IS 'Unique identifier for this domain record'; + +COMMENT ON COLUMN services_public.domains.database_id IS 'Reference to the metaschema database this domain belongs to'; + +COMMENT ON COLUMN services_public.domains.api_id IS 'API endpoint this domain routes to (mutually exclusive with site_id)'; + +COMMENT ON COLUMN services_public.domains.site_id IS 'Site this domain routes to (mutually exclusive with api_id)'; + +COMMENT ON COLUMN services_public.domains.subdomain IS 'Subdomain portion of the hostname'; + +COMMENT ON COLUMN services_public.domains.domain IS 'Root domain of the hostname'; + +CREATE INDEX domains_database_id_idx ON services_public.domains (database_id); + +CREATE INDEX domains_api_id_idx ON services_public.domains (api_id); + +CREATE INDEX domains_site_id_idx ON services_public.domains (site_id); + +CREATE TABLE services_public.site_metadata ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + title text, + description text, + og_image image, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CHECK (character_length(title) <= 120), + CHECK (character_length(description) <= 120) +); + +COMMENT ON TABLE services_public.site_metadata IS 'SEO and social sharing metadata for a site: page title, description, and Open Graph image'; + +COMMENT ON COLUMN services_public.site_metadata.id IS 'Unique identifier for this metadata record'; + +COMMENT ON COLUMN services_public.site_metadata.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.site_metadata.site_id IS 'Site this metadata belongs to'; + +COMMENT ON COLUMN services_public.site_metadata.title IS 'Page title for SEO (max 120 characters)'; + +COMMENT ON COLUMN services_public.site_metadata.description IS 'Meta description for SEO and social sharing (max 120 characters)'; + +COMMENT ON COLUMN services_public.site_metadata.og_image IS 'Open Graph image for social media previews'; + +ALTER TABLE services_public.site_metadata + ADD CONSTRAINT site_metadata_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX site_metadata_site_id_idx ON services_public.site_metadata (site_id); + +CREATE INDEX site_metadata_database_id_idx ON services_public.site_metadata (database_id); + +CREATE TABLE services_public.site_modules ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + name text NOT NULL, + data pg_catalog.json NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.site_modules IS 'Site-level module configuration; stores module name and JSON settings used by the frontend or server for each site'; + +COMMENT ON COLUMN services_public.site_modules.id IS 'Unique identifier for this site module record'; + +COMMENT ON COLUMN services_public.site_modules.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.site_modules.site_id IS 'Site this module configuration belongs to'; + +COMMENT ON COLUMN services_public.site_modules.name IS 'Module name (e.g. user_auth_module, analytics)'; + +COMMENT ON COLUMN services_public.site_modules.data IS 'JSON configuration data for this module'; + +ALTER TABLE services_public.site_modules + ADD CONSTRAINT site_modules_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX site_modules_site_id_idx ON services_public.site_modules (site_id); + +CREATE INDEX site_modules_database_id_idx ON services_public.site_modules (database_id); + +CREATE TABLE services_public.site_themes ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + site_id uuid NOT NULL, + theme jsonb NOT NULL, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.site_themes IS 'Theme configuration for a site; stores design tokens, colors, and typography as JSONB'; + +COMMENT ON COLUMN services_public.site_themes.id IS 'Unique identifier for this theme record'; + +COMMENT ON COLUMN services_public.site_themes.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.site_themes.site_id IS 'Site this theme belongs to'; + +COMMENT ON COLUMN services_public.site_themes.theme IS 'JSONB object containing theme tokens (colors, typography, spacing, etc.)'; + +ALTER TABLE services_public.site_themes + ADD CONSTRAINT site_themes_site_id_fkey + FOREIGN KEY(site_id) + REFERENCES services_public.sites (id); + +CREATE INDEX site_themes_site_id_idx ON services_public.site_themes (site_id); + +CREATE INDEX site_themes_database_id_idx ON services_public.site_themes (database_id); + +CREATE FUNCTION services_private.tg_enforce_api_table_name_uniqueness() RETURNS trigger AS $EOFCODE$ +DECLARE + new_name_hash bytea; + conflicting_api_name text; + conflicting_table_name text; +BEGIN + -- Compute the plural-hash of the new table name + new_name_hash := metaschema_private.table_name_hash(NEW.name); + + -- Check if any API that includes this table's schema also includes + -- another schema containing a table with the same name hash + SELECT a.name, t.name + INTO conflicting_api_name, conflicting_table_name + FROM services_public.api_schemas AS my_api + JOIN services_public.api_schemas AS other_api + ON other_api.api_id = my_api.api_id + AND other_api.schema_id IS DISTINCT FROM NEW.schema_id + JOIN metaschema_public.table AS t + ON t.schema_id = other_api.schema_id + AND metaschema_private.table_name_hash(t.name) = new_name_hash + JOIN services_public.apis AS a + ON a.id = my_api.api_id + WHERE my_api.schema_id = NEW.schema_id + LIMIT 1; + + IF conflicting_api_name IS NOT NULL THEN + RAISE EXCEPTION 'Table name "%" conflicts with existing table "%" in API "%". Table names must be unique (by plural form) across all schemas within the same API.', + NEW.name, conflicting_table_name, conflicting_api_name; + END IF; + + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER _000003_enforce_api_table_name_uniqueness + BEFORE INSERT + ON metaschema_public."table" + FOR EACH ROW + EXECUTE PROCEDURE services_private.tg_enforce_api_table_name_uniqueness(); + +CREATE TRIGGER _000003_enforce_api_table_name_uniqueness_update + BEFORE UPDATE + ON metaschema_public."table" + FOR EACH ROW + WHEN (new.name IS DISTINCT FROM old.name + OR new.schema_id IS DISTINCT FROM old.schema_id) + EXECUTE PROCEDURE services_private.tg_enforce_api_table_name_uniqueness(); + +CREATE FUNCTION services_private.tg_enforce_api_schema_table_name_uniqueness() RETURNS trigger AS $EOFCODE$ +DECLARE + conflicting_new_table text; + conflicting_existing_table text; +BEGIN + -- Find any table name collision between the newly linked schema + -- and any schema already linked to the same API + SELECT new_t.name, existing_t.name + INTO conflicting_new_table, conflicting_existing_table + FROM metaschema_public.table AS new_t + JOIN services_public.api_schemas AS existing_link + ON existing_link.api_id = NEW.api_id + AND existing_link.schema_id IS DISTINCT FROM NEW.schema_id + JOIN metaschema_public.table AS existing_t + ON existing_t.schema_id = existing_link.schema_id + AND metaschema_private.table_name_hash(existing_t.name) = metaschema_private.table_name_hash(new_t.name) + WHERE new_t.schema_id = NEW.schema_id + LIMIT 1; + + IF conflicting_new_table IS NOT NULL THEN + RAISE EXCEPTION 'Cannot link schema to API: table "%" conflicts with existing table "%" already exposed in this API. Table names must be unique (by plural form) across all schemas within the same API.', + conflicting_new_table, conflicting_existing_table; + END IF; + + RETURN NEW; +END; +$EOFCODE$ LANGUAGE plpgsql VOLATILE; + +CREATE TRIGGER _000001_enforce_api_schema_table_name_uniqueness + BEFORE INSERT + ON services_public.api_schemas + FOR EACH ROW + EXECUTE PROCEDURE services_private.tg_enforce_api_schema_table_name_uniqueness(); + +CREATE TABLE services_public.database_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + enable_aggregates boolean NOT NULL DEFAULT false, + enable_postgis boolean NOT NULL DEFAULT true, + enable_search boolean NOT NULL DEFAULT true, + enable_direct_uploads boolean NOT NULL DEFAULT true, + enable_presigned_uploads boolean NOT NULL DEFAULT true, + enable_many_to_many boolean NOT NULL DEFAULT true, + enable_connection_filter boolean NOT NULL DEFAULT true, + 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, + enable_i18n boolean NOT NULL DEFAULT false, + options jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.database_settings IS 'Database-wide feature flags and settings; controls which platform features are available to all APIs in this database'; + +COMMENT ON COLUMN services_public.database_settings.id IS 'Unique identifier for this settings record'; + +COMMENT ON COLUMN services_public.database_settings.database_id IS 'Reference to the metaschema database these settings apply to'; + +COMMENT ON COLUMN services_public.database_settings.enable_aggregates IS 'Enable aggregate queries (sum, avg, min, max, etc.) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_postgis IS 'Enable PostGIS spatial types and operators in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_search IS 'Enable unified search (tsvector, BM25, pg_trgm, pgvector) in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_direct_uploads IS 'Enable direct (multipart) file upload mutations in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_presigned_uploads IS 'Enable presigned URL upload flow for S3/MinIO storage'; + +COMMENT ON COLUMN services_public.database_settings.enable_many_to_many IS 'Enable many-to-many relationship queries in the GraphQL API'; + +COMMENT ON COLUMN services_public.database_settings.enable_connection_filter IS 'Enable connection filter (where argument) in the GraphQL API'; + +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.enable_i18n IS 'Enable internationalization plugin (localeStrings field, translation table discovery) 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); + +CREATE TABLE services_public.api_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid NOT NULL UNIQUE, + enable_aggregates boolean, + enable_postgis boolean, + enable_search boolean, + enable_direct_uploads boolean, + enable_presigned_uploads boolean, + enable_many_to_many boolean, + enable_connection_filter boolean, + enable_ltree boolean, + enable_llm boolean, + enable_realtime boolean, + enable_bulk boolean, + enable_i18n boolean, + options jsonb NOT NULL DEFAULT '{}'::jsonb, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE +); + +COMMENT ON TABLE services_public.api_settings IS 'Per-API feature flag overrides; NULL columns inherit from database_settings, explicit true/false overrides the database default'; + +COMMENT ON COLUMN services_public.api_settings.id IS 'Unique identifier for this API settings record'; + +COMMENT ON COLUMN services_public.api_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.api_settings.api_id IS 'API these settings override for'; + +COMMENT ON COLUMN services_public.api_settings.enable_aggregates IS 'Override: enable aggregate queries (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_postgis IS 'Override: enable PostGIS spatial types (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_search IS 'Override: enable unified search (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_direct_uploads IS 'Override: enable direct (multipart) file uploads (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_presigned_uploads IS 'Override: enable presigned URL upload flow (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_many_to_many IS 'Override: enable many-to-many relationships (NULL = inherit from database_settings)'; + +COMMENT ON COLUMN services_public.api_settings.enable_connection_filter IS 'Override: enable connection filter (NULL = inherit from database_settings)'; + +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.enable_i18n IS 'Override: enable internationalization plugin (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); + +CREATE INDEX api_settings_api_id_idx ON services_public.api_settings (api_id); + +CREATE TABLE services_public.rls_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + authenticate_schema_id uuid, + role_schema_id uuid, + authenticate_function_id uuid, + authenticate_strict_function_id uuid, + current_role_function_id uuid, + current_role_id_function_id uuid, + current_user_agent_function_id uuid, + current_ip_address_function_id uuid, + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT authenticate_schema_fkey + FOREIGN KEY(authenticate_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT role_schema_fkey + FOREIGN KEY(role_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT authenticate_function_fkey + FOREIGN KEY(authenticate_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT authenticate_strict_function_fkey + FOREIGN KEY(authenticate_strict_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_role_function_fkey + FOREIGN KEY(current_role_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_role_id_function_fkey + FOREIGN KEY(current_role_id_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_user_agent_function_fkey + FOREIGN KEY(current_user_agent_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT current_ip_address_function_fkey + FOREIGN KEY(current_ip_address_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.rls_settings IS 'Per-database RLS module runtime configuration; typed replacement for api_modules rls_module JSONB entries'; + +COMMENT ON COLUMN services_public.rls_settings.id IS 'Unique identifier for this RLS settings record'; + +COMMENT ON COLUMN services_public.rls_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.rls_settings.authenticate_schema_id IS 'Schema containing authenticate/authenticate_strict functions (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.rls_settings.role_schema_id IS 'Schema containing current_role and related functions (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.rls_settings.authenticate_function_id IS 'Reference to the authenticate function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.authenticate_strict_function_id IS 'Reference to the strict authenticate function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_role_function_id IS 'Reference to the current_role function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_role_id_function_id IS 'Reference to the current_role_id function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_user_agent_function_id IS 'Reference to the current_user_agent function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.rls_settings.current_ip_address_function_id IS 'Reference to the current_ip_address function (FK to metaschema_public.function)'; + +CREATE INDEX rls_settings_database_id_idx ON services_public.rls_settings (database_id); + +CREATE TABLE services_public.cors_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL, + api_id uuid, + allowed_origins text[] NOT NULL DEFAULT '{}', + CONSTRAINT db_fkey + FOREIGN KEY(database_id) + REFERENCES metaschema_public.database (id) + ON DELETE CASCADE, + CONSTRAINT api_fkey + FOREIGN KEY(api_id) + REFERENCES services_public.apis (id) + ON DELETE CASCADE, + CONSTRAINT cors_settings_unique + UNIQUE (database_id, api_id) +); + +COMMENT ON TABLE services_public.cors_settings IS 'Per-database and per-API CORS origin configuration; typed replacement for api_modules cors JSONB entries'; + +COMMENT ON COLUMN services_public.cors_settings.id IS 'Unique identifier for this CORS settings record'; + +COMMENT ON COLUMN services_public.cors_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.cors_settings.api_id IS 'Optional API for per-API override; NULL means database-wide default'; + +COMMENT ON COLUMN services_public.cors_settings.allowed_origins IS 'Array of allowed CORS origins (e.g. https://example.com)'; + +CREATE INDEX cors_settings_database_id_idx ON services_public.cors_settings (database_id); + +CREATE INDEX cors_settings_api_id_idx ON services_public.cors_settings (api_id); + +CREATE TABLE services_public.pubkey_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + schema_id uuid, + crypto_network text NOT NULL DEFAULT 'cosmos', + user_field text NOT NULL DEFAULT 'user_id', + sign_up_with_key_function_id uuid, + sign_in_request_challenge_function_id uuid, + sign_in_record_failure_function_id uuid, + sign_in_with_challenge_function_id uuid, + 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 SET NULL, + CONSTRAINT sign_up_with_key_function_fkey + FOREIGN KEY(sign_up_with_key_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT sign_in_request_challenge_function_fkey + FOREIGN KEY(sign_in_request_challenge_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT sign_in_record_failure_function_fkey + FOREIGN KEY(sign_in_record_failure_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL, + CONSTRAINT sign_in_with_challenge_function_fkey + FOREIGN KEY(sign_in_with_challenge_function_id) + REFERENCES metaschema_public.function (id) + ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.pubkey_settings IS 'Per-database public-key crypto auth runtime configuration; typed replacement for api_modules pubkey_challenge JSONB entries'; + +COMMENT ON COLUMN services_public.pubkey_settings.id IS 'Unique identifier for this pubkey settings record'; + +COMMENT ON COLUMN services_public.pubkey_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.pubkey_settings.schema_id IS 'Schema containing the crypto auth functions (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.pubkey_settings.crypto_network IS 'Crypto network for key derivation (e.g. cosmos, ethereum)'; + +COMMENT ON COLUMN services_public.pubkey_settings.user_field IS 'Field name used to identify the user in crypto auth functions'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_up_with_key_function_id IS 'Reference to the sign-up-with-key function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_request_challenge_function_id IS 'Reference to the sign-in challenge request function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_record_failure_function_id IS 'Reference to the sign-in failure recording function (FK to metaschema_public.function)'; + +COMMENT ON COLUMN services_public.pubkey_settings.sign_in_with_challenge_function_id IS 'Reference to the sign-in-with-challenge function (FK to metaschema_public.function)'; + +CREATE INDEX pubkey_settings_database_id_idx ON services_public.pubkey_settings (database_id); + +CREATE TABLE services_public.webauthn_settings ( + id uuid PRIMARY KEY DEFAULT uuidv7(), + database_id uuid NOT NULL UNIQUE, + schema_id uuid, + credentials_schema_id uuid, + sessions_schema_id uuid, + session_secrets_schema_id uuid, + credentials_table_id uuid, + sessions_table_id uuid, + session_credentials_table_id uuid, + session_secrets_table_id uuid, + user_field_id uuid, + rp_id text NOT NULL DEFAULT '', + rp_name text NOT NULL DEFAULT '', + origin_allowlist text[] NOT NULL DEFAULT '{}', + attestation_type text NOT NULL DEFAULT 'none' CHECK (attestation_type IN ('none', 'indirect', 'direct', 'enterprise')), + require_user_verification boolean NOT NULL DEFAULT false, + resident_key text NOT NULL DEFAULT 'required' CHECK (resident_key IN ('discouraged', 'preferred', 'required')), + challenge_expiry_seconds bigint NOT NULL DEFAULT 300, + 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 SET NULL, + CONSTRAINT credentials_schema_fkey + FOREIGN KEY(credentials_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT sessions_schema_fkey + FOREIGN KEY(sessions_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT session_secrets_schema_fkey + FOREIGN KEY(session_secrets_schema_id) + REFERENCES metaschema_public.schema (id) + ON DELETE SET NULL, + CONSTRAINT credentials_table_fkey + FOREIGN KEY(credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT sessions_table_fkey + FOREIGN KEY(sessions_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT session_credentials_table_fkey + FOREIGN KEY(session_credentials_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT session_secrets_table_fkey + FOREIGN KEY(session_secrets_table_id) + REFERENCES metaschema_public."table" (id) + ON DELETE SET NULL, + CONSTRAINT user_field_fkey + FOREIGN KEY(user_field_id) + REFERENCES metaschema_public.field (id) + ON DELETE SET NULL +); + +COMMENT ON TABLE services_public.webauthn_settings IS 'Per-database WebAuthn/passkey runtime configuration; typed replacement for api_modules webauthn_challenge JSONB entries'; + +COMMENT ON COLUMN services_public.webauthn_settings.id IS 'Unique identifier for this WebAuthn settings record'; + +COMMENT ON COLUMN services_public.webauthn_settings.database_id IS 'Reference to the metaschema database'; + +COMMENT ON COLUMN services_public.webauthn_settings.schema_id IS 'Schema containing WebAuthn auth procedures (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.credentials_schema_id IS 'Schema of the webauthn_credentials table (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.sessions_schema_id IS 'Schema of the sessions table (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.session_secrets_schema_id IS 'Schema of the session_secrets table (FK to metaschema_public.schema)'; + +COMMENT ON COLUMN services_public.webauthn_settings.credentials_table_id IS 'Reference to the webauthn_credentials table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.sessions_table_id IS 'Reference to the sessions table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.session_credentials_table_id IS 'Reference to the session_credentials table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.session_secrets_table_id IS 'Reference to the session_secrets table (FK to metaschema_public.table)'; + +COMMENT ON COLUMN services_public.webauthn_settings.user_field_id IS 'Reference to the user field on webauthn_credentials (FK to metaschema_public.field)'; + +COMMENT ON COLUMN services_public.webauthn_settings.rp_id IS 'WebAuthn Relying Party ID (typically the domain name)'; + +COMMENT ON COLUMN services_public.webauthn_settings.rp_name IS 'WebAuthn Relying Party display name'; + +COMMENT ON COLUMN services_public.webauthn_settings.origin_allowlist IS 'Allowed origins for WebAuthn registration and authentication'; + +COMMENT ON COLUMN services_public.webauthn_settings.attestation_type IS 'Attestation conveyance preference (none, indirect, direct, enterprise)'; + +COMMENT ON COLUMN services_public.webauthn_settings.require_user_verification IS 'Whether to require user verification (biometric/PIN) during auth'; + +COMMENT ON COLUMN services_public.webauthn_settings.resident_key IS 'Resident key requirement (discouraged, preferred, required)'; + +COMMENT ON COLUMN services_public.webauthn_settings.challenge_expiry_seconds IS 'Challenge TTL in seconds (default 300 = 5 minutes)'; + +CREATE INDEX webauthn_settings_database_id_idx ON services_public.webauthn_settings (database_id); \ No newline at end of file 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 b5f46e8e..1c026102 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 @@ -17,6 +17,7 @@ SELECT enable_llm, enable_realtime, enable_bulk, + enable_i18n, 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 64c15c82..50ff73c2 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 @@ -16,6 +16,7 @@ SELECT enable_llm, enable_realtime, enable_bulk, + enable_i18n, options FROM services_public.database_settings WHERE false;