* add response updateResponse * add singleUpsertResponse * add delete/invoke response * add open api response * add info response * remove ApiRequest from profileHeader * contentTypeHeaders only needs ApiRequest
481 lines
19 KiB
Haskell
481 lines
19 KiB
Haskell
{-|
|
|
Module : PostgREST.App
|
|
Description : PostgREST main application
|
|
|
|
This module is in charge of mapping HTTP requests to PostgreSQL queries.
|
|
Some of its functionality includes:
|
|
|
|
- Mapping HTTP request methods to proper SQL statements. For example, a GET request is translated to executing a SELECT query in a read-only TRANSACTION.
|
|
- Producing HTTP Headers according to RFCs.
|
|
- Content Negotiation
|
|
-}
|
|
{-# LANGUAGE RecordWildCards #-}
|
|
module PostgREST.App
|
|
( SignalHandlerInstaller
|
|
, SocketRunner
|
|
, postgrest
|
|
, run
|
|
) where
|
|
|
|
|
|
import Control.Monad.Except (liftEither)
|
|
import Data.Either.Combinators (mapLeft)
|
|
import Data.List (union)
|
|
import Data.Maybe (fromJust)
|
|
import Data.String (IsString (..))
|
|
import Network.Wai.Handler.Warp (defaultSettings, setHost, setPort,
|
|
setServerName)
|
|
import System.Posix.Types (FileMode)
|
|
|
|
import qualified Data.HashMap.Strict as HM
|
|
import qualified Hasql.DynamicStatements.Snippet as SQL (Snippet)
|
|
import qualified Hasql.Transaction as SQL
|
|
import qualified Hasql.Transaction.Sessions as SQL
|
|
import qualified Network.Wai as Wai
|
|
import qualified Network.Wai.Handler.Warp as Warp
|
|
|
|
import qualified PostgREST.Admin as Admin
|
|
import qualified PostgREST.AppState as AppState
|
|
import qualified PostgREST.Auth as Auth
|
|
import qualified PostgREST.Cors as Cors
|
|
import qualified PostgREST.DbStructure as DbStructure
|
|
import qualified PostgREST.Error as Error
|
|
import qualified PostgREST.Logger as Logger
|
|
import qualified PostgREST.Middleware as Middleware
|
|
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
|
import qualified PostgREST.Query.Statements as Statements
|
|
import qualified PostgREST.RangeQuery as RangeQuery
|
|
import qualified PostgREST.Request.ApiRequest as ApiRequest
|
|
import qualified PostgREST.Request.DbRequestBuilder as ReqBuilder
|
|
import qualified PostgREST.Request.Types as ApiRequestTypes
|
|
import qualified PostgREST.Response as Response
|
|
|
|
import PostgREST.AppState (AppState)
|
|
import PostgREST.Auth (AuthResult (..))
|
|
import PostgREST.Config (AppConfig (..),
|
|
LogLevel (..),
|
|
OpenAPIMode (..))
|
|
import PostgREST.Config.PgVersion (PgVersion (..))
|
|
import PostgREST.DbStructure (DbStructure (..))
|
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
|
QualifiedIdentifier (..),
|
|
Schema)
|
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
|
ProcVolatility (..))
|
|
import PostgREST.DbStructure.Table (Table (..))
|
|
import PostgREST.Error (Error)
|
|
import PostgREST.MediaType (MTPlanAttrs (..),
|
|
MediaType (..))
|
|
import PostgREST.Query.Statements (ResultSet (..))
|
|
import PostgREST.Request.ApiRequest (Action (..),
|
|
ApiRequest (..),
|
|
InvokeMethod (..),
|
|
Mutation (..), Target (..))
|
|
import PostgREST.Request.Preferences (PreferCount (..),
|
|
PreferParameters (..),
|
|
PreferRepresentation (..),
|
|
shouldCount)
|
|
import PostgREST.Request.ReadQuery (ReadRequest, fstFieldNames)
|
|
import PostgREST.Version (prettyVersion)
|
|
import PostgREST.Workers (connectionWorker, listener)
|
|
|
|
import qualified PostgREST.DbStructure.Proc as Proc
|
|
|
|
import Protolude hiding (Handler)
|
|
|
|
data RequestContext = RequestContext
|
|
{ ctxConfig :: AppConfig
|
|
, ctxDbStructure :: DbStructure
|
|
, ctxApiRequest :: ApiRequest
|
|
, ctxPgVersion :: PgVersion
|
|
}
|
|
|
|
type Handler = ExceptT Error
|
|
|
|
type DbHandler = Handler SQL.Transaction
|
|
|
|
type SignalHandlerInstaller = AppState -> IO()
|
|
|
|
type SocketRunner = Warp.Settings -> Wai.Application -> FileMode -> FilePath -> IO()
|
|
|
|
|
|
run :: SignalHandlerInstaller -> Maybe SocketRunner -> AppState -> IO ()
|
|
run installHandlers maybeRunWithSocket appState = do
|
|
conf@AppConfig{..} <- AppState.getConfig appState
|
|
connectionWorker appState -- Loads the initial DbStructure
|
|
installHandlers appState
|
|
-- reload schema cache + config on NOTIFY
|
|
when configDbChannelEnabled $ listener appState
|
|
|
|
let app = postgrest configLogLevel appState (connectionWorker appState)
|
|
adminApp = Admin.postgrestAdmin appState conf
|
|
|
|
whenJust configAdminServerPort $ \adminPort -> do
|
|
AppState.logWithZTime appState $ "Admin server listening on port " <> show adminPort
|
|
void . forkIO $ Warp.runSettings (serverSettings conf & setPort adminPort) adminApp
|
|
|
|
case configServerUnixSocket of
|
|
Just socket ->
|
|
-- run the postgrest application with user defined socket. Only for UNIX systems
|
|
case maybeRunWithSocket of
|
|
Just runWithSocket -> do
|
|
AppState.logWithZTime appState $ "Listening on unix socket " <> show socket
|
|
runWithSocket (serverSettings conf) app configServerUnixSocketMode socket
|
|
Nothing ->
|
|
panic "Cannot run with unix socket on non-unix platforms."
|
|
Nothing ->
|
|
do
|
|
AppState.logWithZTime appState $ "Listening on port " <> show configServerPort
|
|
Warp.runSettings (serverSettings conf) app
|
|
where
|
|
whenJust :: Applicative m => Maybe a -> (a -> m ()) -> m ()
|
|
whenJust mg f = maybe (pure ()) f mg
|
|
|
|
serverSettings :: AppConfig -> Warp.Settings
|
|
serverSettings AppConfig{..} =
|
|
defaultSettings
|
|
& setHost (fromString $ toS configServerHost)
|
|
& setPort configServerPort
|
|
& setServerName ("postgrest/" <> prettyVersion)
|
|
|
|
-- | PostgREST application
|
|
postgrest :: LogLevel -> AppState.AppState -> IO () -> Wai.Application
|
|
postgrest logLevel appState connWorker =
|
|
Cors.middleware .
|
|
Auth.middleware appState .
|
|
Logger.middleware logLevel $
|
|
-- fromJust can be used, because the auth middleware will **always** add
|
|
-- some AuthResult to the vault.
|
|
\req respond -> case fromJust $ Auth.getResult req of
|
|
Left err -> respond $ Error.errorResponseFor err
|
|
Right authResult -> do
|
|
conf <- AppState.getConfig appState
|
|
maybeDbStructure <- AppState.getDbStructure appState
|
|
pgVer <- AppState.getPgVersion appState
|
|
jsonDbS <- AppState.getJsonDbS appState
|
|
|
|
let
|
|
eitherResponse :: IO (Either Error Wai.Response)
|
|
eitherResponse =
|
|
runExceptT $ postgrestResponse appState conf maybeDbStructure jsonDbS pgVer authResult req
|
|
|
|
response <- either Error.errorResponseFor identity <$> eitherResponse
|
|
-- Launch the connWorker when the connection is down. The postgrest
|
|
-- function can respond successfully (with a stale schema cache) before
|
|
-- the connWorker is done.
|
|
when (Response.isServiceUnavailable response) connWorker
|
|
resp <- do
|
|
delay <- AppState.getRetryNextIn appState
|
|
return $ Response.addRetryHint delay response
|
|
respond resp
|
|
|
|
postgrestResponse
|
|
:: AppState.AppState
|
|
-> AppConfig
|
|
-> Maybe DbStructure
|
|
-> ByteString
|
|
-> PgVersion
|
|
-> AuthResult
|
|
-> Wai.Request
|
|
-> Handler IO Wai.Response
|
|
postgrestResponse appState conf@AppConfig{..} maybeDbStructure jsonDbS pgVer AuthResult{..} req = do
|
|
body <- lift $ Wai.strictRequestBody req
|
|
|
|
dbStructure <-
|
|
case maybeDbStructure of
|
|
Just dbStructure ->
|
|
return dbStructure
|
|
Nothing ->
|
|
throwError Error.NoSchemaCacheError
|
|
|
|
apiRequest <-
|
|
liftEither . mapLeft Error.ApiRequestError $
|
|
ApiRequest.userApiRequest conf dbStructure req body
|
|
|
|
let ctx apiReq = RequestContext conf dbStructure apiReq pgVer
|
|
|
|
if iAction apiRequest == ActionInfo then
|
|
pure $ Response.infoResponse (iTarget apiRequest) dbStructure
|
|
else
|
|
runDbHandler appState (txMode apiRequest) (Just authRole /= configDbAnonRole) configDbPreparedStatements .
|
|
Middleware.optionalRollback conf apiRequest $
|
|
Middleware.runPgLocals conf authClaims authRole (handleRequest . ctx) apiRequest jsonDbS pgVer
|
|
|
|
runDbHandler :: AppState.AppState -> SQL.Mode -> Bool -> Bool -> DbHandler b -> Handler IO b
|
|
runDbHandler appState mode authenticated prepared handler = do
|
|
dbResp <-
|
|
let transaction = if prepared then SQL.transaction else SQL.unpreparedTransaction in
|
|
lift . AppState.usePool appState . transaction SQL.ReadCommitted mode $ runExceptT handler
|
|
|
|
resp <-
|
|
liftEither . mapLeft Error.PgErr $
|
|
mapLeft (Error.PgError authenticated) dbResp
|
|
|
|
liftEither resp
|
|
|
|
handleRequest :: RequestContext -> DbHandler Wai.Response
|
|
handleRequest context@(RequestContext _ _ ApiRequest{..} _) =
|
|
case (iAction, iTarget) of
|
|
(ActionRead headersOnly, TargetIdent identifier) ->
|
|
handleRead headersOnly identifier context
|
|
(ActionMutate MutationCreate, TargetIdent identifier) ->
|
|
handleCreate identifier context
|
|
(ActionMutate MutationUpdate, TargetIdent identifier) ->
|
|
handleUpdate identifier context
|
|
(ActionMutate MutationSingleUpsert, TargetIdent identifier) ->
|
|
handleSingleUpsert identifier context
|
|
(ActionMutate MutationDelete, TargetIdent identifier) ->
|
|
handleDelete identifier context
|
|
(ActionInvoke invMethod, TargetProc proc _) ->
|
|
handleInvoke invMethod proc context
|
|
(ActionInspect headersOnly, TargetDefaultSpec tSchema) ->
|
|
handleOpenApi headersOnly tSchema context
|
|
_ ->
|
|
-- This is unreachable as the ApiRequest.hs rejects it before
|
|
-- TODO Refactor the Action/Target types to remove this line
|
|
throwError $ Error.ApiRequestError ApiRequestTypes.NotFound
|
|
|
|
handleRead :: Bool -> QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
|
handleRead headersOnly identifier context@RequestContext{..} = do
|
|
req <- readRequest identifier context
|
|
bField <- binaryField context req
|
|
|
|
let
|
|
ApiRequest{..} = ctxApiRequest
|
|
AppConfig{..} = ctxConfig
|
|
countQuery = QueryBuilder.readRequestToCountQuery req
|
|
|
|
resultSet <-
|
|
lift . SQL.statement mempty $
|
|
Statements.prepareRead
|
|
(QueryBuilder.readRequestToQuery req)
|
|
(if iPreferCount == Just EstimatedCount then
|
|
-- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed
|
|
QueryBuilder.limitedQuery countQuery ((+ 1) <$> configDbMaxRows)
|
|
else
|
|
countQuery
|
|
)
|
|
(shouldCount iPreferCount)
|
|
iAcceptMediaType
|
|
bField
|
|
configDbPreparedStatements
|
|
|
|
failNotSingular iAcceptMediaType resultSet
|
|
total <- readTotal ctxConfig ctxApiRequest resultSet countQuery
|
|
|
|
pure $ Response.readResponse headersOnly identifier ctxApiRequest total resultSet
|
|
|
|
readTotal :: AppConfig -> ApiRequest -> ResultSet -> SQL.Snippet -> DbHandler (Maybe Int64)
|
|
readTotal _ _ RSPlan{} _ = pure Nothing
|
|
readTotal AppConfig{..} ApiRequest{..} RSStandard{rsTableTotal=tableTotal} countQuery =
|
|
case iPreferCount of
|
|
Just PlannedCount ->
|
|
explain
|
|
Just EstimatedCount ->
|
|
if tableTotal > (fromIntegral <$> configDbMaxRows) then
|
|
max tableTotal <$> explain
|
|
else
|
|
return tableTotal
|
|
_ ->
|
|
return tableTotal
|
|
where
|
|
explain =
|
|
lift . SQL.statement mempty . Statements.preparePlanRows countQuery $
|
|
configDbPreparedStatements
|
|
|
|
handleCreate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
|
handleCreate identifier context@RequestContext{..} = do
|
|
let
|
|
ApiRequest{..} = ctxApiRequest
|
|
pkCols = if iPreferRepresentation /= None || isJust iPreferResolution
|
|
then maybe mempty tablePKCols $ HM.lookup identifier $ dbTables ctxDbStructure
|
|
else mempty
|
|
|
|
resultSet <- writeQuery MutationCreate identifier True pkCols context
|
|
|
|
failNotSingular iAcceptMediaType resultSet
|
|
|
|
pure $ Response.createResponse identifier pkCols ctxApiRequest resultSet
|
|
|
|
handleUpdate :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
|
handleUpdate identifier context@(RequestContext _ _ ctxApiRequest@ApiRequest{..} _) = do
|
|
resultSet <- writeQuery MutationUpdate identifier False mempty context
|
|
failNotSingular iAcceptMediaType resultSet
|
|
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
|
|
|
|
pure $ Response.updateResponse ctxApiRequest resultSet
|
|
|
|
handleSingleUpsert :: QualifiedIdentifier -> RequestContext-> DbHandler Wai.Response
|
|
handleSingleUpsert identifier context@(RequestContext _ ctxDbStructure ctxApiRequest _) = do
|
|
let pkCols = maybe mempty tablePKCols $ HM.lookup identifier $ dbTables ctxDbStructure
|
|
resultSet <- writeQuery MutationSingleUpsert identifier False pkCols context
|
|
failPut resultSet
|
|
pure $ Response.singleUpsertResponse ctxApiRequest resultSet
|
|
|
|
-- Makes sure the querystring pk matches the payload pk
|
|
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted,
|
|
-- PUT /items?id=eq.14 { "id" : 2, .. } is rejected.
|
|
-- If this condition is not satisfied then nothing is inserted,
|
|
-- check the WHERE for INSERT in QueryBuilder.hs to see how it's done
|
|
failPut :: ResultSet -> DbHandler ()
|
|
failPut RSPlan{} = pure ()
|
|
failPut RSStandard{rsQueryTotal=queryTotal} =
|
|
when (queryTotal /= 1) $ do
|
|
lift SQL.condemn
|
|
throwError Error.PutMatchingPkError
|
|
|
|
handleDelete :: QualifiedIdentifier -> RequestContext -> DbHandler Wai.Response
|
|
handleDelete identifier context@(RequestContext _ _ ctxApiRequest@ApiRequest{..} _) = do
|
|
resultSet <- writeQuery MutationDelete identifier False mempty context
|
|
failNotSingular iAcceptMediaType resultSet
|
|
failsChangesOffLimits (RangeQuery.rangeLimit iTopLevelRange) resultSet
|
|
|
|
pure $ Response.deleteResponse ctxApiRequest resultSet
|
|
|
|
handleInvoke :: InvokeMethod -> ProcDescription -> RequestContext -> DbHandler Wai.Response
|
|
handleInvoke invMethod proc context@RequestContext{..} = do
|
|
let
|
|
ApiRequest{..} = ctxApiRequest
|
|
|
|
identifier =
|
|
QualifiedIdentifier
|
|
(pdSchema proc)
|
|
(fromMaybe (pdName proc) $ Proc.procTableName proc)
|
|
|
|
req <- readRequest identifier context
|
|
bField <- binaryField context req
|
|
|
|
let callReq = ReqBuilder.callRequest proc ctxApiRequest req
|
|
|
|
resultSet <-
|
|
lift . SQL.statement mempty $
|
|
Statements.prepareCall
|
|
(Proc.procReturnsScalar proc)
|
|
(Proc.procReturnsSingle proc)
|
|
(QueryBuilder.requestToCallProcQuery callReq)
|
|
(QueryBuilder.readRequestToQuery req)
|
|
(QueryBuilder.readRequestToCountQuery req)
|
|
(shouldCount iPreferCount)
|
|
iAcceptMediaType
|
|
(iPreferParameters == Just MultipleObjects)
|
|
bField
|
|
(configDbPreparedStatements ctxConfig)
|
|
|
|
failNotSingular iAcceptMediaType resultSet
|
|
pure $ Response.invokeResponse invMethod proc ctxApiRequest resultSet
|
|
|
|
handleOpenApi :: Bool -> Schema -> RequestContext -> DbHandler Wai.Response
|
|
handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure apiRequest ctxPgVersion) = do
|
|
body <-
|
|
lift $ case configOpenApiMode of
|
|
OAFollowPriv ->
|
|
Just <$> ((,,)
|
|
<$> SQL.statement [tSchema] (DbStructure.accessibleTables ctxPgVersion configDbPreparedStatements)
|
|
<*> SQL.statement tSchema (DbStructure.accessibleProcs ctxPgVersion configDbPreparedStatements)
|
|
<*> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements))
|
|
OAIgnorePriv ->
|
|
Just <$> ((,,)
|
|
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ DbStructure.dbTables dbStructure)
|
|
(HM.filterWithKey (\(QualifiedIdentifier sch _) _ -> sch == tSchema) $ DbStructure.dbProcs dbStructure)
|
|
<$> SQL.statement tSchema (DbStructure.schemaDescription configDbPreparedStatements))
|
|
OADisabled ->
|
|
pure Nothing
|
|
|
|
pure $ Response.openApiResponse headersOnly body conf dbStructure $ iProfile apiRequest
|
|
|
|
txMode :: ApiRequest -> SQL.Mode
|
|
txMode ApiRequest{..} =
|
|
case (iAction, iTarget) of
|
|
(ActionRead _, _) ->
|
|
SQL.Read
|
|
(ActionInfo, _) ->
|
|
SQL.Read
|
|
(ActionInspect _, _) ->
|
|
SQL.Read
|
|
(ActionInvoke InvGet, _) ->
|
|
SQL.Read
|
|
(ActionInvoke InvHead, _) ->
|
|
SQL.Read
|
|
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Stable} _) ->
|
|
SQL.Read
|
|
(ActionInvoke InvPost, TargetProc ProcDescription{pdVolatility=Immutable} _) ->
|
|
SQL.Read
|
|
_ ->
|
|
SQL.Write
|
|
|
|
writeQuery :: Mutation -> QualifiedIdentifier -> Bool -> [Text] -> RequestContext -> DbHandler ResultSet
|
|
writeQuery mutation identifier@QualifiedIdentifier{..} isInsert pkCols context@RequestContext{..} = do
|
|
readReq <- readRequest identifier context
|
|
|
|
mutateReq <-
|
|
liftEither $
|
|
ReqBuilder.mutateRequest mutation qiSchema qiName ctxApiRequest
|
|
pkCols
|
|
readReq
|
|
|
|
lift . SQL.statement mempty $
|
|
Statements.prepareWrite
|
|
(QueryBuilder.readRequestToQuery readReq)
|
|
(QueryBuilder.mutateRequestToQuery mutateReq)
|
|
isInsert
|
|
(iAcceptMediaType ctxApiRequest)
|
|
(iPreferRepresentation ctxApiRequest)
|
|
pkCols
|
|
(configDbPreparedStatements ctxConfig)
|
|
|
|
-- |
|
|
-- Fail a response if a single JSON object was requested and not exactly one
|
|
-- was found.
|
|
failNotSingular :: MediaType -> ResultSet -> DbHandler ()
|
|
failNotSingular _ RSPlan{} = pure ()
|
|
failNotSingular mediaType RSStandard{rsQueryTotal=queryTotal} =
|
|
when (mediaType == MTSingularJSON && queryTotal /= 1) $ do
|
|
lift SQL.condemn
|
|
throwError $ Error.singularityError queryTotal
|
|
|
|
failsChangesOffLimits :: Maybe Integer -> ResultSet -> DbHandler ()
|
|
failsChangesOffLimits _ RSPlan{} = pure ()
|
|
failsChangesOffLimits Nothing _ = pure ()
|
|
failsChangesOffLimits (Just maxChanges) RSStandard{rsQueryTotal=queryTotal} =
|
|
when (queryTotal > fromIntegral maxChanges) $ do
|
|
lift SQL.condemn
|
|
throwError $ Error.OffLimitsChangesError queryTotal maxChanges
|
|
|
|
returnsScalar :: ApiRequest.Target -> Bool
|
|
returnsScalar (TargetProc proc _) = Proc.procReturnsScalar proc
|
|
returnsScalar _ = False
|
|
|
|
readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest
|
|
readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure apiRequest _) =
|
|
liftEither $
|
|
ReqBuilder.readRequest qiSchema qiName configDbMaxRows
|
|
(dbRelationships dbStructure)
|
|
apiRequest
|
|
|
|
-- | If raw(binary) output is requested, check that MediaType is one of the
|
|
-- admitted rawMediaTypes and that`?select=...` contains only one field other
|
|
-- than `*`
|
|
binaryField :: Monad m => RequestContext -> ReadRequest -> Handler m (Maybe FieldName)
|
|
binaryField RequestContext{..} readReq
|
|
| returnsScalar (iTarget ctxApiRequest) && isRawMediaType =
|
|
return $ Just "pgrst_scalar"
|
|
| isRawMediaType =
|
|
let
|
|
fldNames = fstFieldNames readReq
|
|
fieldName = headMay fldNames
|
|
in
|
|
if length fldNames == 1 && fieldName /= Just "*" then
|
|
return fieldName
|
|
else
|
|
throwError $ Error.BinaryFieldError mediaType
|
|
| otherwise =
|
|
return Nothing
|
|
where
|
|
mediaType = iAcceptMediaType ctxApiRequest
|
|
isRawMediaType = mediaType `elem` configRawMediaTypes ctxConfig `union` [MTOctetStream, MTTextPlain, MTTextXML] || isRawPlan mediaType
|
|
isRawPlan mt = case mt of
|
|
MTPlan (MTPlanAttrs (Just MTOctetStream) _ _) -> True
|
|
MTPlan (MTPlanAttrs (Just MTTextPlain) _ _) -> True
|
|
MTPlan (MTPlanAttrs (Just MTTextXML) _ _) -> True
|
|
_ -> False
|