Complete bounded browser and API product behavior

This commit is contained in:
ms2k 2026-07-19 01:12:29 +09:00
commit ea55108f44

View file

@ -3,6 +3,9 @@ module Nuthatch.Api
, HealthEndpoint
, ReadyEndpoint
, BrowserScriptEndpoint
, AppEndpoint
, StaticAssetsEndpoint
, ProductApi
, LoginEndpoint
, LoginStart
, OidcCallbackEndpoint
@ -33,6 +36,12 @@ module Nuthatch.Api
, DeleteRequest (..)
, DeletePrefixRequest (..)
, DeletePrefixResponse (..)
, SessionResponse (..)
, DeletePlanResponse (..)
, DeleteBatchItem (..)
, DeleteBatchRequest (..)
, DeleteItemResponse (..)
, DeleteBatchResponse (..)
, UploadResponse (..)
, DeleteResponse (..)
, Throws
@ -296,6 +305,7 @@ instance ToJSON BrowseError where
data DownloadError
= DownloadObjectStoreFailed Text
| DownloadNotFound
| DownloadRangeUnsupported
| DownloadThrottleUnavailable
| DownloadBandwidthExceeded
deriving stock (Eq, Show)
@ -305,6 +315,8 @@ instance ToJSON DownloadError where
errorValue "object_store_error" message Nothing
toJSON DownloadNotFound =
errorValue "not_found" "object not found" Nothing
toJSON DownloadRangeUnsupported =
errorValue "range_not_supported" "Range downloads are not supported" Nothing
toJSON DownloadThrottleUnavailable =
errorValue "throttle_unavailable" "bandwidth throttle unavailable" Nothing
toJSON DownloadBandwidthExceeded =
@ -361,6 +373,82 @@ instance ToJSON DeletePrefixError where
toJSON (DeletePrefixMutationRejected message) =
errorValue "prefix_mutation_rejected" message Nothing
data SessionResponse = SessionResponse
{ sessionResponseUserId :: UserId
, sessionResponseDisplay :: Maybe Text
, sessionResponseCsrf :: CsrfToken
}
deriving stock (Eq, Show)
instance ToJSON SessionResponse where
toJSON response = object
[ "user_id" .= sessionResponseUserId response
, "display" .= sessionResponseDisplay response
, "csrf" .= sessionResponseCsrf response
]
data DeletePlanResponse = DeletePlanResponse
{ deletePlanPaths :: [RelativePath]
, deletePlanContinuation :: Maybe PaginationToken
}
deriving stock (Eq, Show)
instance ToJSON DeletePlanResponse where
toJSON response = object
[ "paths" .= map unRelativePath (deletePlanPaths response)
, "continuation" .= deletePlanContinuation response
]
data DeleteBatchItem = DeleteBatchItem
{ deleteBatchPath :: RelativePath
, deleteBatchOperationId :: OperationId
}
deriving stock (Eq, Show)
instance FromJSON DeleteBatchItem where
parseJSON = withObject "DeleteBatchItem" $ \o ->
DeleteBatchItem
<$> o .: "path"
<*> o .: "operation_id"
data DeleteBatchRequest = DeleteBatchRequest
{ deleteBatchRoot :: Root
, deleteBatchCsrf :: CsrfToken
, deleteBatchItems :: [DeleteBatchItem]
}
deriving stock (Eq, Show)
instance FromJSON DeleteBatchRequest where
parseJSON = withObject "DeleteBatchRequest" $ \o ->
DeleteBatchRequest
<$> o .: "root"
<*> o .: "csrf"
<*> o .: "items"
data DeleteItemResponse = DeleteItemResponse
{ deleteItemPath :: RelativePath
, deleteItemOperationId :: OperationId
, deleteItemResult :: Maybe MutationResult
, deleteItemError :: Maybe Text
}
deriving stock (Eq, Show)
instance ToJSON DeleteItemResponse where
toJSON response = object
[ "path" .= unRelativePath (deleteItemPath response)
, "operation_id" .= deleteItemOperationId response
, "result" .= fmap mutationResultText (deleteItemResult response)
, "error" .= deleteItemError response
]
newtype DeleteBatchResponse = DeleteBatchResponse
{ deleteBatchResults :: [DeleteItemResponse]
}
deriving stock (Eq, Show)
instance ToJSON DeleteBatchResponse where
toJSON response = object ["results" .= deleteBatchResults response]
errorValue :: Text -> Text -> Maybe Value -> Value
errorValue code message details = object
[ "error" .= code
@ -377,6 +465,19 @@ type ReadyEndpoint =
type BrowserScriptEndpoint =
"nuthatch.js" :> Get '[JavaScript] BrowserScript
type AppEndpoint =
"app"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Get '[HTML]
(Headers '[Header "Content-Security-Policy" Text] HtmlPage)
type StaticAssetsEndpoint =
"assets"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Raw
type LoginStart =
Headers
'[ Header "Set-Cookie" Text
@ -437,6 +538,7 @@ type DownloadSuccess =
Headers
'[ Header "Content-Disposition" Text
, Header "X-Content-Type-Options" Text
, Header "Content-Security-Policy" Text
, Header "Content-Length" Integer
]
(SourceIO BS.ByteString)
@ -448,6 +550,7 @@ type DownloadEndpoint =
:> Throws DownloadError
:> QueryParam' '[Required, Strict] "root" Root
:> QueryParam' '[Required, Strict] "path" RelativePath
:> Header "Range" Text
:> StreamGet NoFraming OctetStream DownloadSuccess
type UploadEndpoint =
@ -480,10 +583,69 @@ type DeletePrefixEndpoint =
:> ReqBody '[JSON] DeletePrefixRequest
:> Post '[JSON] DeletePrefixResponse
type ProductApi =
"api" :>
( "session"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Get '[JSON] SessionResponse
:<|> "browse"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws BrowseError
:> QueryParam "root" Root
:> QueryParam "prefix" BrowsePrefix
:> QueryParam "q" SearchQuery
:> QueryParam "continuation" PaginationToken
:> Get '[JSON] BrowseView
:<|> "download"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws DownloadError
:> QueryParam' '[Required, Strict] "root" Root
:> QueryParam' '[Required, Strict] "path" RelativePath
:> Header "Range" Text
:> StreamGet NoFraming OctetStream DownloadSuccess
:<|> "upload"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws UploadError
:> QueryParam' '[Required, Strict] "root" Root
:> QueryParam' '[Required, Strict] "path" RelativePath
:> QueryParam' '[Required, Strict] "operation_id" OperationId
:> Header' '[Required, Strict] "X-Declared-Size" DeclaredSize
:> Header' '[Required, Strict] "X-Content-SHA256" ContentSha256
:> Header' '[Required, Strict] "X-CSRF-Token" CsrfToken
:> StreamBody NoFraming OctetStream (SourceIO BS.ByteString)
:> Post '[JSON] UploadResponse
:<|> "delete"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws DeleteError
:> ReqBody '[JSON] DeleteRequest
:> Post '[JSON] DeleteResponse
:<|> "delete-plan"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws DeletePrefixError
:> QueryParam' '[Required, Strict] "root" Root
:> QueryParam' '[Required, Strict] "path" RelativePath
:> QueryParam "continuation" PaginationToken
:> Get '[JSON] DeletePlanResponse
:<|> "delete-batch"
:> AuthProtect SessionAuth
:> Throws AuthenticationError
:> Throws DeletePrefixError
:> ReqBody '[JSON] DeleteBatchRequest
:> Post '[JSON] DeleteBatchResponse
)
type Api =
HealthEndpoint
:<|> ReadyEndpoint
:<|> BrowserScriptEndpoint
:<|> AppEndpoint
:<|> StaticAssetsEndpoint
:<|> LoginEndpoint
:<|> OidcCallbackEndpoint
:<|> LogoutEndpoint
@ -492,6 +654,7 @@ type Api =
:<|> UploadEndpoint
:<|> DeleteEndpoint
:<|> DeletePrefixEndpoint
:<|> ProductApi
api :: Proxy Api
api = Proxy