generated from coulomb/repo-seed
Convert all remaining `<- paramOrNothing / param / paramOrDefault /
currentUserOrNothing` monadic binds to `let` — these functions are pure
(ImplicitParams-based) in IHP v1.5, so `<-` is a type error in an IO
do-block.
Controllers fixed:
AgentDelegations, AiGovernancePolicies, Annotations, ApiConsumers,
CollectiveProposals, DecisionRecords, DeploymentRecords,
HubCapabilityManifests, HubRoutingRules, InstitutionalKnowledge,
OutcomeCorrelations, RequirementCandidates, TypeRegistries,
WebhookSubscriptions, Widgets,
Api/V2/{Annotations,InteractionEvents,Token}
WebhookSubscriptions: remove orphaned `Right () ->` case arm that was
left inside a bare `unless` block (structural parse error).
Also carries forward all in-progress fixes from the working tree:
helpers (AgentBridge, ApiRateLimit, BottleneckDetector,
CrossHubPropagation, FrictionScore),
views (CanSelect instances, HSX lambda extraction, formFor wrappers),
env/build (envrc GHCi perms, flake.nix Tailwind + GHC resource limits,
static/app.css additional Tailwind output).
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
170 lines
8.6 KiB
Haskell
170 lines
8.6 KiB
Haskell
module Web.Controller.HubCapabilityManifests where
|
|
|
|
import Web.Types
|
|
import Web.View.HubCapabilityManifests.Index
|
|
import Web.View.HubCapabilityManifests.Show
|
|
import Web.View.HubCapabilityManifests.New
|
|
import Web.View.HubCapabilityManifests.Edit
|
|
import Generated.Types
|
|
import IHP.Prelude
|
|
import IHP.ControllerPrelude
|
|
import Data.Aeson (Value, Array, decode, encode)
|
|
import qualified Data.Vector as V
|
|
import Data.Maybe (mapMaybe)
|
|
|
|
instance Controller HubCapabilityManifestsController where
|
|
beforeAction = ensureIsUser
|
|
|
|
action HubCapabilityManifestsAction = autoRefresh do
|
|
manifests <- query @HubCapabilityManifest
|
|
|> orderByDesc #createdAt
|
|
|> fetch
|
|
hubs <- query @Hub |> fetch
|
|
render IndexView { manifests, hubs }
|
|
|
|
action ShowHubCapabilityManifestAction { hubCapabilityManifestId } = do
|
|
manifest <- fetch hubCapabilityManifestId
|
|
hub <- fetch manifest.hubId
|
|
render ShowView { manifest, hub }
|
|
|
|
action NewHubCapabilityManifestAction = do
|
|
let mHubId = paramOrNothing @(Id Hub) "hubId"
|
|
hubs <- query @Hub |> orderByAsc #name |> fetch
|
|
let manifest = newRecord @HubCapabilityManifest
|
|
case mHubId of
|
|
Just hubId -> do
|
|
-- Check if a manifest already exists for this hub
|
|
existing <- query @HubCapabilityManifest
|
|
|> filterWhere (#hubId, hubId)
|
|
|> fetchOneOrNothing
|
|
case existing of
|
|
Just m -> redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId = m.id }
|
|
Nothing -> render NewView { manifest = manifest |> set #hubId hubId, hubs }
|
|
Nothing -> render NewView { manifest, hubs }
|
|
|
|
action CreateHubCapabilityManifestAction = do
|
|
hubs <- query @Hub |> orderByAsc #name |> fetch
|
|
let manifest = newRecord @HubCapabilityManifest
|
|
manifest
|
|
|> fill @'["hubId", "manifestVersion", "capabilityDescription", "contact"]
|
|
|> set #status "draft"
|
|
|> validateField #hubId nonEmpty
|
|
|> ifValid \case
|
|
Left manifest -> render NewView { manifest, hubs }
|
|
Right manifest -> do
|
|
manifest <- createRecord manifest
|
|
setSuccessMessage "Capability manifest created (draft)"
|
|
redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId = manifest.id }
|
|
|
|
action EditHubCapabilityManifestAction { hubCapabilityManifestId } = do
|
|
manifest <- fetch hubCapabilityManifestId
|
|
hub <- fetch manifest.hubId
|
|
widgetTypeEntries <- sqlQuery "SELECT * FROM widget_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
eventTypeEntries <- sqlQuery "SELECT * FROM event_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
categoryEntries <- sqlQuery "SELECT * FROM annotation_category_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
policyScopeEntries <- sqlQuery "SELECT * FROM policy_scope_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries }
|
|
|
|
action UpdateHubCapabilityManifestAction { hubCapabilityManifestId } = do
|
|
manifest <- fetch hubCapabilityManifestId
|
|
hub <- fetch manifest.hubId
|
|
widgetTypeEntries <- sqlQuery "SELECT * FROM widget_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
eventTypeEntries <- sqlQuery "SELECT * FROM event_type_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
categoryEntries <- sqlQuery "SELECT * FROM annotation_category_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
policyScopeEntries <- sqlQuery "SELECT * FROM policy_scope_registry WHERE status = 'active' ORDER BY label ASC" ()
|
|
when (manifest.status == "active") do
|
|
setErrorMessage "Active manifests are read-only. Retire the current manifest and create a new draft to amend."
|
|
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
|
|
manifest
|
|
|> fill @'["manifestVersion", "capabilityDescription", "contact",
|
|
"declaredWidgetTypes", "declaredEventTypes",
|
|
"declaredAnnotationCategories", "declaredPolicyScopes"]
|
|
|> ifValid \case
|
|
Left manifest -> render EditView { manifest, hub, widgetTypeEntries, eventTypeEntries, categoryEntries, policyScopeEntries }
|
|
Right manifest -> do
|
|
updateRecord manifest
|
|
setSuccessMessage "Manifest updated"
|
|
redirectTo EditHubCapabilityManifestAction { hubCapabilityManifestId }
|
|
|
|
action ActivateManifestAction { hubCapabilityManifestId } = do
|
|
manifest <- fetch hubCapabilityManifestId
|
|
hub <- fetch manifest.hubId
|
|
|
|
-- Collect declared type names from JSONB arrays
|
|
let wTypes = jsonArrayTexts manifest.declaredWidgetTypes
|
|
eTypes = jsonArrayTexts manifest.declaredEventTypes
|
|
cats = jsonArrayTexts manifest.declaredAnnotationCategories
|
|
scopes = jsonArrayTexts manifest.declaredPolicyScopes
|
|
|
|
-- Conflict detection: check that each declared name is either
|
|
-- unregistered or already owned by this hub.
|
|
conflicts <- fmap concat $ mapM (checkConflict "widget_type_registry" hub.id) wTypes
|
|
eConflicts <- fmap concat $ mapM (checkConflict "event_type_registry" hub.id) eTypes
|
|
cConflicts <- fmap concat $ mapM (checkConflict "annotation_category_registry" hub.id) cats
|
|
pConflicts <- fmap concat $ mapM (checkConflict "policy_scope_registry" hub.id) scopes
|
|
let allConflicts = conflicts <> eConflicts <> cConflicts <> pConflicts
|
|
|
|
if not (null allConflicts)
|
|
then do
|
|
setErrorMessage ("Activation blocked — type name conflicts: " <> intercalate ", " allConflicts)
|
|
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
|
|
else do
|
|
-- Register declared types (idempotent — skip if already present)
|
|
mapM_ (upsertType "widget_type_registry" hub.id) wTypes
|
|
mapM_ (upsertType "event_type_registry" hub.id) eTypes
|
|
mapM_ (upsertType "annotation_category_registry" hub.id) cats
|
|
mapM_ (upsertType "policy_scope_registry" hub.id) scopes
|
|
now <- getCurrentTime
|
|
manifest |> set #status "active" |> set #activatedAt (Just now) |> updateRecord
|
|
setSuccessMessage "Manifest activated — all declared types are now registered"
|
|
redirectTo ShowHubCapabilityManifestAction { hubCapabilityManifestId }
|
|
|
|
action RetireManifestAction { hubCapabilityManifestId } = do
|
|
manifest <- fetch hubCapabilityManifestId
|
|
manifest |> set #status "retired" |> updateRecord
|
|
setSuccessMessage "Manifest retired. Types remain in registry but this manifest is no longer current."
|
|
redirectTo HubCapabilityManifestsAction
|
|
|
|
-- | Extract text values from a JSONB array (e.g. '["foo","bar"]').
|
|
jsonArrayTexts :: Value -> [Text]
|
|
jsonArrayTexts val = case val of
|
|
_ -> case decode (encode val) of
|
|
Just (arr :: Array) -> mapMaybe extractText (V.toList arr)
|
|
Nothing -> []
|
|
where
|
|
extractText (String t) = Just t
|
|
extractText _ = Nothing
|
|
|
|
-- | Check if 'name' in 'tableName' is owned by a different hub.
|
|
-- Returns [] if no conflict, or [error message] if conflict.
|
|
checkConflict ::
|
|
(?modelContext :: ModelContext) =>
|
|
Text -> Id Hub -> Text -> IO [Text]
|
|
checkConflict tableName hubId name = do
|
|
rows <- sqlQuery
|
|
("SELECT owner_hub_id FROM " <> tableName <> " WHERE name = ?")
|
|
(Only name)
|
|
case rows of
|
|
[] -> pure []
|
|
[Only Nothing] -> pure [] -- framework-level, no owner conflict
|
|
[Only (Just ownerId)] ->
|
|
if ownerId == hubId
|
|
then pure []
|
|
else pure ["Type '" <> name <> "' in " <> tableName <> " is already owned by another hub"]
|
|
_ -> pure []
|
|
|
|
-- | Insert a type name into the registry table if it doesn't exist.
|
|
upsertType ::
|
|
(?modelContext :: ModelContext) =>
|
|
Text -> Id Hub -> Text -> IO ()
|
|
upsertType tableName hubId name =
|
|
sqlExec
|
|
("INSERT INTO " <> tableName <> " (name, label, owner_hub_id, status) "
|
|
<> "VALUES (?, ?, ?, 'active') ON CONFLICT (name) DO NOTHING")
|
|
(name, name, hubId)
|
|
|
|
intercalate :: Text -> [Text] -> Text
|
|
intercalate _ [] = ""
|
|
intercalate _ [x] = x
|
|
intercalate sep (x:xs) = x <> sep <> intercalate sep xs
|