From ac9f88b5a6ec93efed8bd50f54853b091dbc308a Mon Sep 17 00:00:00 2001 From: Dan Lynch Date: Wed, 19 Aug 2026 16:27:02 +0000 Subject: [PATCH] feat(node-type-registry): sync the CheckOwnedRelation node type from constructive-db --- .../src/blueprint-types.generated.ts | 34 +++++- .../src/data/check-owned-relation.ts | 101 ++++++++++++++++++ packages/node-type-registry/src/data/index.ts | 1 + 3 files changed, 135 insertions(+), 1 deletion(-) create mode 100644 packages/node-type-registry/src/data/check-owned-relation.ts diff --git a/packages/node-type-registry/src/blueprint-types.generated.ts b/packages/node-type-registry/src/blueprint-types.generated.ts index d312b4fca1..39b8908023 100644 --- a/packages/node-type-registry/src/blueprint-types.generated.ts +++ b/packages/node-type-registry/src/blueprint-types.generated.ts @@ -72,6 +72,35 @@ export interface CheckOneOfParams { /* Array of allowed values for the column */ values: string[]; } +/** Declares that typed pointer columns may only reference rows owned by the same scope owner as the referencing row, proven through a link table that carries the owner (owner_scope, owner_key) — typically a catalog projection that must never be referenced structurally. Creates one BEFORE INSERT OR UPDATE trigger enforcing the declared arity over the pointers and requiring every pointer that is set to resolve to a same-owner row, so it covers belongs-to / has-one / has-many (key on the row being written) and has-and-belongs-to-many (both keys on the junction row, arity all). Visibility/publication flags are never an authorization input. */ +export interface CheckOwnedRelationParams { + /* Schema holding the link tables the ownership is proven through (e.g. catalog_private) */ + link_schema_name: string; + /* Scope that owns the referencing rows; matched against the link row's owner_scope (e.g. database, platform) */ + owner_scope: string; + /* Column on the referencing table carrying its scope key, matched against the link row's owner_key. NULL for a global scope, whose owner key is NULL by construction */ + owner_key_field_name?: string; + /* Typed pointer columns to guard, each resolved through its own link table */ + pointers: { + /* Pointer column on the referencing table */field_name: string; + /* Link table in link_schema_name whose ids this column points at */link_table_name: string; + /* Optional link column additionally restricted regardless of ownership (e.g. a bucket type) */target_type_field_name?: string; + /* Value target_type_field_name must equal */target_type_value?: string; + /* Admit the same database's global-scope rows (app/platform, ownerless by construction) for this column, for pointers whose legal target set is a scope hierarchy within one database */allow_owner_tier?: boolean; + }[]; + /* How many pointers a row must set: exactly_one for a polymorphic target (route → api OR site OR bucket), all for a junction/join row of a has-and-belongs-to-many relation (and for a mandatory belongs-to), any for optional pointers that are only checked when set */ + arity?: 'exactly_one' | 'all' | 'any'; + /* Error code raised when the arity rule over the pointers is violated */ + required_error?: string; + /* Error code raised when the target is not owned by the referencing row owner */ + denied_error?: string; + /* Schema the guard trigger function is created in; defaults to the table schema */ + trigger_function_schema?: string; + /* Guard trigger function name; defaults to tg__owned_relation_guard */ + trigger_function_name?: string; + /* Guard trigger name; defaults to
_owned_relation_guard */ + trigger_name?: string; +} /** * =========================================================================== * Data node type parameters @@ -2105,7 +2134,7 @@ export interface BlueprintEntityType { */ ; /** String shorthand -- just the node type name. */ -export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzAppMemberOwner' | 'AuthzAppMembership' | 'AuthzColumnSecurity' | 'AuthzComposite' | 'AuthzDatabaseClaim' | 'AuthzDenyAll' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzFilePath' | 'AuthzHumanOnly' | 'AuthzMemberList' | 'AuthzMemberOwner' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedMemberOwner' | 'AuthzRelatedPeerOwnership' | 'AuthzSystemOnly' | 'AuthzTemporal' | 'AuthzValueAllowed' | 'AuthzValueExists' | 'AuthzValueMatch' | 'CheckGreaterThan' | 'CheckLessThan' | 'CheckNotEqual' | 'CheckOneOf' | 'DataArchivable' | 'DataBulk' | 'DataCapabilities' | 'DataCompositeField' | 'DataDenormalized' | 'DataDirectOwner' | 'DataEntityMembership' | 'DataForceCurrentUser' | 'DataGenerated' | 'DataHistory' | 'DataI18n' | 'DataId' | 'DataIdentity' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'DataJsonb' | 'DataLock' | 'DataMemberOwner' | 'DataOwnedFields' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPrincipalstamps' | 'DataPublishable' | 'DataRealtime' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings' | 'EventReferral' | 'EventTracker' | 'GuardStepUp' | 'JobTrigger' | 'LimitEnforceAggregate' | 'LimitEnforceCounter' | 'LimitEnforceFeature' | 'LimitEnforceRate' | 'LimitTrackUsage' | 'LimitWarningAggregate' | 'LimitWarningCounter' | 'LimitWarningRate' | 'ProcessChunks' | 'ProcessExtraction' | 'ProcessFileEmbedding' | 'ProcessImageEmbedding' | 'ProcessImageVersions'; +export type BlueprintNodeShorthand = 'AuthzAllowAll' | 'AuthzAppMemberOwner' | 'AuthzAppMembership' | 'AuthzColumnSecurity' | 'AuthzComposite' | 'AuthzDatabaseClaim' | 'AuthzDenyAll' | 'AuthzDirectOwner' | 'AuthzDirectOwnerAny' | 'AuthzEntityMembership' | 'AuthzFilePath' | 'AuthzHumanOnly' | 'AuthzMemberList' | 'AuthzMemberOwner' | 'AuthzNotReadOnly' | 'AuthzOrgHierarchy' | 'AuthzPeerOwnership' | 'AuthzPublishable' | 'AuthzRelatedEntityMembership' | 'AuthzRelatedMemberList' | 'AuthzRelatedMemberOwner' | 'AuthzRelatedPeerOwnership' | 'AuthzSystemOnly' | 'AuthzTemporal' | 'AuthzValueAllowed' | 'AuthzValueExists' | 'AuthzValueMatch' | 'CheckGreaterThan' | 'CheckLessThan' | 'CheckNotEqual' | 'CheckOneOf' | 'CheckOwnedRelation' | 'DataArchivable' | 'DataBulk' | 'DataCapabilities' | 'DataCompositeField' | 'DataDenormalized' | 'DataDirectOwner' | 'DataEntityMembership' | 'DataForceCurrentUser' | 'DataGenerated' | 'DataHistory' | 'DataI18n' | 'DataId' | 'DataIdentity' | 'DataImmutableFields' | 'DataInflection' | 'DataInheritFromParent' | 'DataJsonb' | 'DataLock' | 'DataMemberOwner' | 'DataOwnedFields' | 'DataOwnershipInEntity' | 'DataPeoplestamps' | 'DataPrincipalstamps' | 'DataPublishable' | 'DataRealtime' | 'DataSlug' | 'DataSoftDelete' | 'DataStatusField' | 'DataTags' | 'DataTimestamps' | 'SearchBm25' | 'SearchFullText' | 'SearchSpatial' | 'SearchSpatialAggregate' | 'SearchTrgm' | 'SearchUnified' | 'SearchVector' | 'TableOrganizationSettings' | 'TableUserProfiles' | 'TableUserSettings' | 'EventReferral' | 'EventTracker' | 'GuardStepUp' | 'JobTrigger' | 'LimitEnforceAggregate' | 'LimitEnforceCounter' | 'LimitEnforceFeature' | 'LimitEnforceRate' | 'LimitTrackUsage' | 'LimitWarningAggregate' | 'LimitWarningCounter' | 'LimitWarningRate' | 'ProcessChunks' | 'ProcessExtraction' | 'ProcessFileEmbedding' | 'ProcessImageEmbedding' | 'ProcessImageVersions'; /** Object form -- { $type, data } with typed parameters. */ export type BlueprintNodeObject = { $type: 'AuthzAllowAll'; @@ -2200,6 +2229,9 @@ export type BlueprintNodeObject = { } | { $type: 'CheckOneOf'; data: CheckOneOfParams; +} | { + $type: 'CheckOwnedRelation'; + data: CheckOwnedRelationParams; } | { $type: 'DataArchivable'; data: DataArchivableParams; diff --git a/packages/node-type-registry/src/data/check-owned-relation.ts b/packages/node-type-registry/src/data/check-owned-relation.ts new file mode 100644 index 0000000000..33708bf7d6 --- /dev/null +++ b/packages/node-type-registry/src/data/check-owned-relation.ts @@ -0,0 +1,101 @@ +import type { NodeTypeDefinition } from '../types'; + +export const CheckOwnedRelation: NodeTypeDefinition = { + name: 'CheckOwnedRelation', + slug: 'check_owned_relation', + category: 'check', + display_name: 'Check Owned Relation', + description: 'Declares that typed pointer columns may only reference rows owned by the same scope owner as the referencing row, proven through a link table that carries the owner (owner_scope, owner_key) — typically a catalog projection that must never be referenced structurally. Creates one BEFORE INSERT OR UPDATE trigger enforcing the declared arity over the pointers and requiring every pointer that is set to resolve to a same-owner row, so it covers belongs-to / has-one / has-many (key on the row being written) and has-and-belongs-to-many (both keys on the junction row, arity all). Visibility/publication flags are never an authorization input.', + parameter_schema: { + type: 'object', + properties: { + link_schema_name: { + type: 'string', + description: 'Schema holding the link tables the ownership is proven through (e.g. catalog_private)' + }, + owner_scope: { + type: 'string', + description: "Scope that owns the referencing rows; matched against the link row's owner_scope (e.g. database, platform)" + }, + owner_key_field_name: { + type: 'string', + format: 'column-ref', + description: "Column on the referencing table carrying its scope key, matched against the link row's owner_key. NULL for a global scope, whose owner key is NULL by construction" + }, + pointers: { + type: 'array', + description: 'Typed pointer columns to guard, each resolved through its own link table', + items: { + type: 'object', + properties: { + field_name: { + type: 'string', + format: 'column-ref', + description: 'Pointer column on the referencing table' + }, + link_table_name: { + type: 'string', + description: 'Link table in link_schema_name whose ids this column points at' + }, + target_type_field_name: { + type: 'string', + description: 'Optional link column additionally restricted regardless of ownership (e.g. a bucket type)' + }, + target_type_value: { + type: 'string', + description: 'Value target_type_field_name must equal' + }, + allow_owner_tier: { + type: 'boolean', + description: "Admit the same database's global-scope rows (app/platform, ownerless by construction) for this column, for pointers whose legal target set is a scope hierarchy within one database", + default: false + } + }, + required: ['field_name', 'link_table_name'] + } + }, + arity: { + type: 'string', + enum: ['exactly_one', 'all', 'any'], + description: + 'How many pointers a row must set: exactly_one for a polymorphic target (route → api OR site OR bucket), all for a junction/join row of a has-and-belongs-to-many relation (and for a mandatory belongs-to), any for optional pointers that are only checked when set', + default: 'exactly_one' + }, + required_error: { + type: 'string', + description: 'Error code raised when the arity rule over the pointers is violated', + default: 'OWNED_RELATION_TARGET_REQUIRED' + }, + denied_error: { + type: 'string', + description: 'Error code raised when the target is not owned by the referencing row owner', + default: 'OWNED_RELATION_TARGET_NOT_OWNED' + }, + trigger_function_schema: { + type: 'string', + description: 'Schema the guard trigger function is created in; defaults to the table schema' + }, + trigger_function_name: { + type: 'string', + description: + 'Guard trigger function name; defaults to tg_
_owned_relation_guard' + }, + trigger_name: { + type: 'string', + description: 'Guard trigger name; defaults to
_owned_relation_guard' + } + }, + required: [ + 'link_schema_name', + 'owner_scope', + 'pointers' + ] + }, + tags: [ + 'check', + 'validation', + 'ownership', + 'relation', + 'trigger' + ] +}; diff --git a/packages/node-type-registry/src/data/index.ts b/packages/node-type-registry/src/data/index.ts index 577ea4f076..50b0068ae3 100644 --- a/packages/node-type-registry/src/data/index.ts +++ b/packages/node-type-registry/src/data/index.ts @@ -2,6 +2,7 @@ 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 { CheckOwnedRelation } from './check-owned-relation'; export { DataArchivable } from './data-archivable'; export { DataBulk } from './data-bulk'; export { DataCapabilities } from './data-capabilities';