diff --git a/packages/node-type-registry/src/data/check-scoped-foreign-key.ts b/packages/node-type-registry/src/data/check-scoped-foreign-key.ts new file mode 100644 index 000000000..b015ad5e3 --- /dev/null +++ b/packages/node-type-registry/src/data/check-scoped-foreign-key.ts @@ -0,0 +1,59 @@ +import type { NodeTypeDefinition } from '../types'; + +export const CheckScopedForeignKey: NodeTypeDefinition = { + name: 'CheckScopedForeignKey', + slug: 'check_scoped_foreign_key', + category: 'check', + display_name: 'Check Scoped Foreign Key', + description: + 'BEFORE INSERT trigger that validates all FK references resolve to the same scope value (e.g. database_id). Prevents cross-scope linking where a user with access to multiple scopes could create invalid cross-scope references. Works on junction tables (2+ FKs) and child tables (1 FK validated against the row\'s own scope field).', + parameter_schema: { + type: 'object', + properties: { + scope_field: { + type: 'string', + format: 'column-ref', + description: + 'Scope field on this table to validate against (e.g. "database_id"). If set, the trigger also checks that all referenced scope values match NEW.scope_field. If omitted, only checks that all references match each other.', + default: 'database_id' + }, + references: { + type: 'array', + items: { + type: 'object', + properties: { + field: { + type: 'string', + format: 'column-ref', + description: 'FK field on this table (e.g. "view_id")' + }, + ref_table: { + type: 'string', + description: + 'Target table name (e.g. "view"). Schema is resolved from the table registry.' + }, + ref_field: { + type: 'string', + format: 'column-ref', + description: 'PK field on the target table (e.g. "id")', + default: 'id' + }, + ref_scope_field: { + type: 'string', + format: 'column-ref', + description: + 'Scope field on the target table to read (e.g. "database_id")', + default: 'database_id' + } + }, + required: ['field', 'ref_table'] + }, + description: + 'FK references to validate. Each target\'s scope field must resolve to the same value.', + minItems: 1 + } + }, + required: ['references'] + }, + tags: ['check', 'trigger', 'security', 'scope-isolation', 'foreign-key'] +}; diff --git a/packages/node-type-registry/src/data/data-denormalized.ts b/packages/node-type-registry/src/data/data-denormalized.ts new file mode 100644 index 000000000..a67fb5964 --- /dev/null +++ b/packages/node-type-registry/src/data/data-denormalized.ts @@ -0,0 +1,70 @@ +import type { NodeTypeDefinition } from '../types'; + +export const DataDenormalized: NodeTypeDefinition = { + name: 'DataDenormalized', + slug: 'data_denormalized', + category: 'data', + display_name: 'Denormalized Field', + description: 'Creates INSERT and UPDATE triggers that copy field values from a referenced (parent) table into the current table whenever the FK changes. Used to denormalize frequently-read columns (e.g. database_id on junction tables) so that RLS and queries can filter locally without joining.', + parameter_schema: { + type: 'object', + properties: { + field: { + type: 'string', + format: 'column-ref', + description: 'FK field on this table that references the parent row (e.g. view_id)' + }, + set_fields: { + type: 'array', + items: { + type: 'string', + format: 'column-ref' + }, + description: 'Field names on this table to be populated from the parent (e.g. ["database_id"])' + }, + ref_field: { + type: 'string', + format: 'column-ref', + description: 'Field on the parent table that is the FK target (e.g. id)' + }, + ref_fields: { + type: 'array', + items: { + type: 'string', + format: 'column-ref' + }, + description: 'Field names on the parent table to copy from (e.g. ["database_id"])' + }, + use_updates: { + type: 'boolean', + description: 'If true, also creates an UPDATE trigger so changes to the FK re-copy values', + default: true + }, + update_defaults: { + type: 'boolean', + description: 'If true, sets the default value of set_fields to uuid_nil() so they are populated by the trigger', + default: true + }, + func_name: { + type: 'string', + description: 'Custom function name suffix (defaults to the FK field name)' + }, + func_order: { + type: 'integer', + description: 'Trigger ordering (0-padded). Lower numbers fire first', + default: 0 + } + }, + required: [ + 'field', + 'set_fields', + 'ref_field', + 'ref_fields' + ] + }, + tags: [ + 'trigger', + 'denormalization', + 'schema' + ] +}; diff --git a/packages/node-type-registry/src/data/index.ts b/packages/node-type-registry/src/data/index.ts index 11a853131..5ac4b747d 100644 --- a/packages/node-type-registry/src/data/index.ts +++ b/packages/node-type-registry/src/data/index.ts @@ -2,8 +2,10 @@ export { CheckGreaterThan } from './check-greater-than'; export { CheckLessThan } from './check-less-than'; export { CheckNotEqual } from './check-not-equal'; export { CheckOneOf } from './check-one-of'; +export { CheckScopedForeignKey } from './check-scoped-foreign-key'; export { DataBulk } from './data-bulk'; export { DataCompositeField } from './data-composite-field'; +export { DataDenormalized } from './data-denormalized'; export { DataDirectOwner } from './data-direct-owner'; export { DataEntityMembership } from './data-entity-membership'; export { DataForceCurrentUser } from './data-force-current-user';