refactor: Split up Types.hs and logically organize modules (#1793)
This commit is contained in:
+21
-6
@@ -32,13 +32,14 @@ import Text.Printf (hPrintf)
|
|||||||
|
|
||||||
import PostgREST.App (postgrest)
|
import PostgREST.App (postgrest)
|
||||||
import PostgREST.Config
|
import PostgREST.Config
|
||||||
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
import PostgREST.DbStructure (DbStructure, getDbStructure,
|
||||||
import PostgREST.Error (PgError (PgError), checkIsFatal,
|
getPgVersion)
|
||||||
errorPayload)
|
import PostgREST.DbStructure.PgVersion (PgVersion (..),
|
||||||
import PostgREST.Statements (dbSettingsStatement)
|
|
||||||
import PostgREST.Types (ConnectionStatus (..), DbStructure,
|
|
||||||
PgVersion (..), SCacheStatus (..),
|
|
||||||
minimumPgVersion)
|
minimumPgVersion)
|
||||||
|
import PostgREST.Error (PgError (PgError),
|
||||||
|
checkIsFatal, errorPayload)
|
||||||
|
import PostgREST.Query.Statements (dbSettingsStatement)
|
||||||
|
|
||||||
import Protolude hiding (hPutStrLn, head, toS)
|
import Protolude hiding (hPutStrLn, head, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
@@ -48,6 +49,20 @@ import System.Posix.Signals
|
|||||||
import UnixSocket
|
import UnixSocket
|
||||||
#endif
|
#endif
|
||||||
|
|
||||||
|
|
||||||
|
-- | Current database connection status data ConnectionStatus
|
||||||
|
data ConnectionStatus
|
||||||
|
= NotConnected
|
||||||
|
| Connected PgVersion
|
||||||
|
| FatalConnectionError Text
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
-- | Schema cache status
|
||||||
|
data SCacheStatus
|
||||||
|
= SCLoaded
|
||||||
|
| SCOnRetry
|
||||||
|
| SCFatalFail
|
||||||
|
|
||||||
-- | This is where everything starts.
|
-- | This is where everything starts.
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
|
|||||||
+18
-10
@@ -34,24 +34,32 @@ library
|
|||||||
default-extensions: OverloadedStrings
|
default-extensions: OverloadedStrings
|
||||||
NoImplicitPrelude
|
NoImplicitPrelude
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
exposed-modules: PostgREST.ApiRequest
|
exposed-modules: PostgREST.App
|
||||||
PostgREST.App
|
|
||||||
PostgREST.Auth
|
PostgREST.Auth
|
||||||
PostgREST.Config
|
PostgREST.Config
|
||||||
PostgREST.DbRequestBuilder
|
PostgREST.Config.JSPath
|
||||||
|
PostgREST.Config.Proxy
|
||||||
|
PostgREST.ContentType
|
||||||
PostgREST.DbStructure
|
PostgREST.DbStructure
|
||||||
|
PostgREST.DbStructure.Identifiers
|
||||||
|
PostgREST.DbStructure.PgVersion
|
||||||
|
PostgREST.DbStructure.Proc
|
||||||
|
PostgREST.DbStructure.Relation
|
||||||
|
PostgREST.DbStructure.Table
|
||||||
PostgREST.Error
|
PostgREST.Error
|
||||||
|
PostgREST.GucHeader
|
||||||
PostgREST.Middleware
|
PostgREST.Middleware
|
||||||
PostgREST.OpenAPI
|
PostgREST.OpenAPI
|
||||||
PostgREST.Parsers
|
PostgREST.Query.QueryBuilder
|
||||||
PostgREST.QueryBuilder
|
PostgREST.Query.SqlFragment
|
||||||
PostgREST.Statements
|
PostgREST.Query.Statements
|
||||||
PostgREST.RangeQuery
|
PostgREST.RangeQuery
|
||||||
PostgREST.Types
|
PostgREST.Request.ApiRequest
|
||||||
|
PostgREST.Request.DbRequestBuilder
|
||||||
|
PostgREST.Request.Parsers
|
||||||
|
PostgREST.Request.Preferences
|
||||||
|
PostgREST.Request.Types
|
||||||
other-modules: Paths_postgrest
|
other-modules: Paths_postgrest
|
||||||
PostgREST.Private.Common
|
|
||||||
PostgREST.Private.ProxyUri
|
|
||||||
PostgREST.Private.QueryFragment
|
|
||||||
build-depends: base >= 4.9 && < 4.15
|
build-depends: base >= 4.9 && < 4.15
|
||||||
, HTTP >= 4000.3.7 && < 4000.4
|
, HTTP >= 4000.3.7 && < 4000.4
|
||||||
, Ranged-sets >= 0.3 && < 0.5
|
, Ranged-sets >= 0.3 && < 0.5
|
||||||
|
|||||||
+36
-16
@@ -30,23 +30,43 @@ import qualified Network.HTTP.Types.Status as HTTP
|
|||||||
import qualified Network.HTTP.Types.URI as HTTP
|
import qualified Network.HTTP.Types.URI as HTTP
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
|
|
||||||
import qualified PostgREST.ApiRequest as ApiRequest
|
|
||||||
import qualified PostgREST.Auth as Auth
|
import qualified PostgREST.Auth as Auth
|
||||||
import qualified PostgREST.DbRequestBuilder as ReqBuilder
|
|
||||||
import qualified PostgREST.DbStructure as DbStructure
|
import qualified PostgREST.DbStructure as DbStructure
|
||||||
import qualified PostgREST.Error as Error
|
import qualified PostgREST.Error as Error
|
||||||
import qualified PostgREST.Middleware as Middleware
|
import qualified PostgREST.Middleware as Middleware
|
||||||
import qualified PostgREST.OpenAPI as OpenAPI
|
import qualified PostgREST.OpenAPI as OpenAPI
|
||||||
import qualified PostgREST.QueryBuilder as QueryBuilder
|
import qualified PostgREST.Query.QueryBuilder as QueryBuilder
|
||||||
|
import qualified PostgREST.Query.Statements as Statements
|
||||||
import qualified PostgREST.RangeQuery as RangeQuery
|
import qualified PostgREST.RangeQuery as RangeQuery
|
||||||
import qualified PostgREST.Statements as Statements
|
import qualified PostgREST.Request.ApiRequest as ApiRequest
|
||||||
|
import qualified PostgREST.Request.DbRequestBuilder as ReqBuilder
|
||||||
|
|
||||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..),
|
import PostgREST.Config (AppConfig (..),
|
||||||
InvokeMethod (..), Target (..))
|
LogLevel (..))
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.ContentType (ContentType (..))
|
||||||
|
import PostgREST.DbStructure (DbStructure (..),
|
||||||
|
tablePKCols)
|
||||||
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier (..),
|
||||||
|
Schema)
|
||||||
|
import PostgREST.DbStructure.Proc (ProcDescription (..),
|
||||||
|
ProcVolatility (..))
|
||||||
|
import PostgREST.DbStructure.Table (Table (..))
|
||||||
import PostgREST.Error (Error)
|
import PostgREST.Error (Error)
|
||||||
|
import PostgREST.GucHeader (GucHeader,
|
||||||
|
addHeadersIfNotIncluded,
|
||||||
|
unwrapGucHeader)
|
||||||
|
import PostgREST.Request.ApiRequest (Action (..),
|
||||||
|
ApiRequest (..),
|
||||||
|
InvokeMethod (..),
|
||||||
|
Target (..))
|
||||||
|
import PostgREST.Request.Preferences (PreferCount (..),
|
||||||
|
PreferParameters (..),
|
||||||
|
PreferRepresentation (..))
|
||||||
|
import PostgREST.Request.Types (ReadRequest, fstFieldNames)
|
||||||
|
|
||||||
import PostgREST.Types
|
import qualified PostgREST.ContentType as ContentType
|
||||||
|
import qualified PostgREST.DbStructure.Proc as Proc
|
||||||
|
|
||||||
import Protolude hiding (Handler, toS)
|
import Protolude hiding (Handler, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
@@ -123,7 +143,7 @@ postgrestResponse conf@AppConfig{..} maybeDbStructure pool time req = do
|
|||||||
Just ct ->
|
Just ct ->
|
||||||
return ct
|
return ct
|
||||||
Nothing ->
|
Nothing ->
|
||||||
throwError . Error.ContentTypeError $ map toMime iAccepts
|
throwError . Error.ContentTypeError $ map ContentType.toMime iAccepts
|
||||||
|
|
||||||
let
|
let
|
||||||
handleReq apiReq =
|
handleReq apiReq =
|
||||||
@@ -352,9 +372,9 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
identifier =
|
identifier =
|
||||||
QualifiedIdentifier
|
QualifiedIdentifier
|
||||||
(pdSchema proc)
|
(pdSchema proc)
|
||||||
(fromMaybe (pdName proc) $ procTableName proc)
|
(fromMaybe (pdName proc) $ Proc.procTableName proc)
|
||||||
|
|
||||||
returnsSingle (ApiRequest.TargetProc target _) = procReturnsSingle target
|
returnsSingle (ApiRequest.TargetProc target _) = Proc.procReturnsSingle target
|
||||||
returnsSingle _ = False
|
returnsSingle _ = False
|
||||||
|
|
||||||
req <- readRequest identifier context
|
req <- readRequest identifier context
|
||||||
@@ -367,7 +387,7 @@ handleInvoke invMethod proc context@RequestContext{..} = do
|
|||||||
(returnsSingle iTarget)
|
(returnsSingle iTarget)
|
||||||
(QueryBuilder.requestToCallProcQuery
|
(QueryBuilder.requestToCallProcQuery
|
||||||
(QualifiedIdentifier (pdSchema proc) (pdName proc))
|
(QualifiedIdentifier (pdSchema proc) (pdName proc))
|
||||||
(specifiedProcArgs iColumns proc)
|
(Proc.specifiedProcArgs iColumns proc)
|
||||||
iPayload
|
iPayload
|
||||||
(returnsScalar iTarget)
|
(returnsScalar iTarget)
|
||||||
iPreferParameters
|
iPreferParameters
|
||||||
@@ -405,7 +425,7 @@ handleOpenApi headersOnly tSchema (RequestContext conf@AppConfig{..} dbStructure
|
|||||||
|
|
||||||
return $
|
return $
|
||||||
Wai.responseLBS HTTP.status200
|
Wai.responseLBS HTTP.status200
|
||||||
(toHeader CTOpenAPI : maybeToList (profileHeader apiRequest))
|
(ContentType.toHeader CTOpenAPI : maybeToList (profileHeader apiRequest))
|
||||||
(if headersOnly then mempty else toS body)
|
(if headersOnly then mempty else toS body)
|
||||||
|
|
||||||
txMode :: ApiRequest -> SQL.Mode
|
txMode :: ApiRequest -> SQL.Mode
|
||||||
@@ -491,7 +511,7 @@ shouldCount preferCount =
|
|||||||
preferCount == Just ExactCount || preferCount == Just EstimatedCount
|
preferCount == Just ExactCount || preferCount == Just EstimatedCount
|
||||||
|
|
||||||
returnsScalar :: ApiRequest.Target -> Bool
|
returnsScalar :: ApiRequest.Target -> Bool
|
||||||
returnsScalar (TargetProc proc _) = procReturnsScalar proc
|
returnsScalar (TargetProc proc _) = Proc.procReturnsScalar proc
|
||||||
returnsScalar _ = False
|
returnsScalar _ = False
|
||||||
|
|
||||||
readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest
|
readRequest :: Monad m => QualifiedIdentifier -> RequestContext -> Handler m ReadRequest
|
||||||
@@ -503,7 +523,7 @@ readRequest QualifiedIdentifier{..} (RequestContext AppConfig{..} dbStructure ap
|
|||||||
|
|
||||||
contentTypeHeaders :: RequestContext -> [HTTP.Header]
|
contentTypeHeaders :: RequestContext -> [HTTP.Header]
|
||||||
contentTypeHeaders RequestContext{..} =
|
contentTypeHeaders RequestContext{..} =
|
||||||
toHeader ctxContentType : maybeToList (profileHeader ctxApiRequest)
|
ContentType.toHeader ctxContentType : maybeToList (profileHeader ctxApiRequest)
|
||||||
|
|
||||||
requestContentTypes :: AppConfig -> ApiRequest -> [ContentType]
|
requestContentTypes :: AppConfig -> ApiRequest -> [ContentType]
|
||||||
requestContentTypes conf ApiRequest{..} =
|
requestContentTypes conf ApiRequest{..} =
|
||||||
@@ -542,7 +562,7 @@ binaryField RequestContext{..} readReq
|
|||||||
|
|
||||||
rawContentTypes :: AppConfig -> [ContentType]
|
rawContentTypes :: AppConfig -> [ContentType]
|
||||||
rawContentTypes AppConfig{..} =
|
rawContentTypes AppConfig{..} =
|
||||||
(decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain]
|
(ContentType.decodeContentType <$> configRawMediaTypes) `union` [CTOctetStream, CTTextPlain]
|
||||||
|
|
||||||
profileHeader :: ApiRequest -> Maybe HTTP.Header
|
profileHeader :: ApiRequest -> Maybe HTTP.Header
|
||||||
profileHeader ApiRequest{..} =
|
profileHeader ApiRequest{..} =
|
||||||
|
|||||||
@@ -11,7 +11,11 @@ In the test suite there is an example of simple login function that can be used
|
|||||||
very simple authentication system inside the PostgreSQL database.
|
very simple authentication system inside the PostgreSQL database.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
module PostgREST.Auth (containsRole, jwtClaims, JWTClaims) where
|
module PostgREST.Auth
|
||||||
|
( containsRole
|
||||||
|
, jwtClaims
|
||||||
|
, JWTClaims
|
||||||
|
) where
|
||||||
|
|
||||||
import qualified Crypto.JWT as JWT
|
import qualified Crypto.JWT as JWT
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
@@ -23,9 +27,8 @@ import Control.Monad.Except (liftEither)
|
|||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.Time.Clock (UTCTime)
|
import Data.Time.Clock (UTCTime)
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..), JSPath, JSPathExp (..))
|
||||||
import PostgREST.Error (Error (..))
|
import PostgREST.Error (Error (..))
|
||||||
import PostgREST.Types (JSPath, JSPathExp (..))
|
|
||||||
|
|
||||||
import Protolude
|
import Protolude
|
||||||
|
|
||||||
|
|||||||
+53
-42
@@ -24,9 +24,11 @@ Other hardcoded options such as the minimum version number also belong here.
|
|||||||
module PostgREST.Config
|
module PostgREST.Config
|
||||||
( prettyVersion
|
( prettyVersion
|
||||||
, docsVersion
|
, docsVersion
|
||||||
|
, LogLevel(..)
|
||||||
, CLI (..)
|
, CLI (..)
|
||||||
, Command (..)
|
, Command (..)
|
||||||
, AppConfig (..)
|
, AppConfig (..)
|
||||||
|
, Proxy(..)
|
||||||
, configDbPoolTimeout'
|
, configDbPoolTimeout'
|
||||||
, dumpAppConfig
|
, dumpAppConfig
|
||||||
, Environment
|
, Environment
|
||||||
@@ -36,6 +38,10 @@ module PostgREST.Config
|
|||||||
, readDbUriFile
|
, readDbUriFile
|
||||||
, readSecretFile
|
, readSecretFile
|
||||||
, parseSecret
|
, parseSecret
|
||||||
|
, JSPath
|
||||||
|
, JSPathExp(..)
|
||||||
|
, isMalformedProxyUri
|
||||||
|
, toURI
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Crypto.JOSE.Types as JOSE
|
import qualified Crypto.JOSE.Types as JOSE
|
||||||
@@ -46,6 +52,9 @@ import qualified Data.ByteString.Base64 as B64
|
|||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.Configurator as C
|
import qualified Data.Configurator as C
|
||||||
import qualified Data.Map.Strict as M
|
import qualified Data.Map.Strict as M
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
|
import qualified GHC.Show (show)
|
||||||
|
|
||||||
import Control.Lens (preview)
|
import Control.Lens (preview)
|
||||||
import Control.Monad (fail)
|
import Control.Monad (fail)
|
||||||
@@ -56,30 +65,24 @@ import Data.List (lookup)
|
|||||||
import Data.List.NonEmpty (fromList, toList)
|
import Data.List.NonEmpty (fromList, toList)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.Scientific (floatingOrInteger)
|
import Data.Scientific (floatingOrInteger)
|
||||||
import Data.Text (dropEnd, dropWhileEnd, filter,
|
|
||||||
intercalate, pack, replace, splitOn,
|
|
||||||
strip, stripPrefix, take, toLower,
|
|
||||||
toTitle, unpack)
|
|
||||||
import Data.Version (versionBranch)
|
import Data.Version (versionBranch)
|
||||||
import Development.GitRev (gitHash)
|
import Development.GitRev (gitHash)
|
||||||
import Numeric (readOct, showOct)
|
import Numeric (readOct, showOct)
|
||||||
|
import Options.Applicative (Parser, customExecParser, flag,
|
||||||
|
footer, fullDesc, help, helper, info,
|
||||||
|
infoOption, long, metavar, prefs,
|
||||||
|
progDesc, short, showHelpOnEmpty,
|
||||||
|
showHelpOnError, strArgument)
|
||||||
import Paths_postgrest (version)
|
import Paths_postgrest (version)
|
||||||
import System.Environment (getEnvironment)
|
import System.Environment (getEnvironment)
|
||||||
import System.Posix.Types (FileMode)
|
import System.Posix.Types (FileMode)
|
||||||
|
|
||||||
import Control.Applicative
|
|
||||||
import Data.Monoid
|
|
||||||
import Options.Applicative hiding (str)
|
|
||||||
import Text.Heredoc (str)
|
import Text.Heredoc (str)
|
||||||
|
|
||||||
import PostgREST.Parsers (pRoleClaimKey)
|
import PostgREST.Config.JSPath (JSPath, JSPathExp (..), pRoleClaimKey)
|
||||||
import PostgREST.Private.ProxyUri (isMalformedProxyUri)
|
import PostgREST.Config.Proxy (Proxy (..), isMalformedProxyUri,
|
||||||
import PostgREST.Types (JSPath, JSPathExp (..),
|
toURI)
|
||||||
LogLevel (..))
|
|
||||||
import Protolude hiding (concat, filter, hPutStrLn,
|
import Protolude hiding (Proxy, toList, toS)
|
||||||
intercalate, null, replace, take,
|
|
||||||
toList, toLower, toS, toTitle,
|
|
||||||
(<>))
|
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
-- | Command line interface options
|
-- | Command line interface options
|
||||||
@@ -128,19 +131,27 @@ configDbPoolTimeout' :: (Fractional a) => AppConfig -> a
|
|||||||
configDbPoolTimeout' =
|
configDbPoolTimeout' =
|
||||||
fromRational . toRational . configDbPoolTimeout
|
fromRational . toRational . configDbPoolTimeout
|
||||||
|
|
||||||
|
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
||||||
|
|
||||||
|
instance Show LogLevel where
|
||||||
|
show LogCrit = "crit"
|
||||||
|
show LogError = "error"
|
||||||
|
show LogWarn = "warn"
|
||||||
|
show LogInfo = "info"
|
||||||
|
|
||||||
-- | User friendly version number
|
-- | User friendly version number
|
||||||
prettyVersion :: Text
|
prettyVersion :: Text
|
||||||
prettyVersion =
|
prettyVersion =
|
||||||
intercalate "." (map show $ versionBranch version) <> gitRev
|
T.intercalate "." (map show $ versionBranch version) <> gitRev
|
||||||
where
|
where
|
||||||
gitRev =
|
gitRev =
|
||||||
if $(gitHash) == "UNKNOWN"
|
if $(gitHash) == "UNKNOWN"
|
||||||
then mempty
|
then mempty
|
||||||
else " (" <> take 7 $(gitHash) <> ")"
|
else " (" <> T.take 7 $(gitHash) <> ")"
|
||||||
|
|
||||||
-- | Version number used in docs
|
-- | Version number used in docs
|
||||||
docsVersion :: Text
|
docsVersion :: Text
|
||||||
docsVersion = "v" <> dropEnd 1 (dropWhileEnd (/= '.') prettyVersion)
|
docsVersion = "v" <> T.dropEnd 1 (T.dropWhileEnd (/= '.') prettyVersion)
|
||||||
|
|
||||||
-- | Read command line interface options. Also prints help.
|
-- | Read command line interface options. Also prints help.
|
||||||
readCLIShowHelp :: Environment -> IO CLI
|
readCLIShowHelp :: Environment -> IO CLI
|
||||||
@@ -280,36 +291,36 @@ dumpAppConfig conf =
|
|||||||
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
pgrstSettings = (\(k, v) -> (k, v conf)) <$>
|
||||||
[("db-anon-role", q . configDbAnonRole)
|
[("db-anon-role", q . configDbAnonRole)
|
||||||
,("db-channel", q . configDbChannel)
|
,("db-channel", q . configDbChannel)
|
||||||
,("db-channel-enabled", toLower . show . configDbChannelEnabled)
|
,("db-channel-enabled", T.toLower . show . configDbChannelEnabled)
|
||||||
,("db-extra-search-path", q . intercalate "," . configDbExtraSearchPath)
|
,("db-extra-search-path", q . T.intercalate "," . configDbExtraSearchPath)
|
||||||
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
,("db-max-rows", maybe "\"\"" show . configDbMaxRows)
|
||||||
,("db-pool", show . configDbPoolSize)
|
,("db-pool", show . configDbPoolSize)
|
||||||
,("db-pool-timeout", show . configDbPoolTimeout)
|
,("db-pool-timeout", show . configDbPoolTimeout)
|
||||||
,("db-pre-request", q . fromMaybe mempty . configDbPreRequest)
|
,("db-pre-request", q . fromMaybe mempty . configDbPreRequest)
|
||||||
,("db-prepared-statements", toLower . show . configDbPreparedStatements)
|
,("db-prepared-statements", T.toLower . show . configDbPreparedStatements)
|
||||||
,("db-root-spec", q . fromMaybe mempty . configDbRootSpec)
|
,("db-root-spec", q . fromMaybe mempty . configDbRootSpec)
|
||||||
,("db-schemas", q . intercalate "," . toList . configDbSchemas)
|
,("db-schemas", q . T.intercalate "," . toList . configDbSchemas)
|
||||||
,("db-config", q . toLower . show . configDbConfig)
|
,("db-config", q . T.toLower . show . configDbConfig)
|
||||||
,("db-tx-end", q . showTxEnd)
|
,("db-tx-end", q . showTxEnd)
|
||||||
,("db-uri", q . configDbUri)
|
,("db-uri", q . configDbUri)
|
||||||
,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience)
|
,("jwt-aud", toS . encode . maybe "" toJSON . configJwtAudience)
|
||||||
,("jwt-role-claim-key", q . intercalate mempty . fmap show . configJwtRoleClaimKey)
|
,("jwt-role-claim-key", q . T.intercalate mempty . fmap show . configJwtRoleClaimKey)
|
||||||
,("jwt-secret", q . toS . showJwtSecret)
|
,("jwt-secret", q . toS . showJwtSecret)
|
||||||
,("jwt-secret-is-base64", toLower . show . configJwtSecretIsBase64)
|
,("jwt-secret-is-base64", T.toLower . show . configJwtSecretIsBase64)
|
||||||
,("log-level", q . show . configLogLevel)
|
,("log-level", q . show . configLogLevel)
|
||||||
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
,("openapi-server-proxy-uri", q . fromMaybe mempty . configOpenApiServerProxyUri)
|
||||||
,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes)
|
,("raw-media-types", q . toS . B.intercalate "," . configRawMediaTypes)
|
||||||
,("server-host", q . configServerHost)
|
,("server-host", q . configServerHost)
|
||||||
,("server-port", show . configServerPort)
|
,("server-port", show . configServerPort)
|
||||||
,("server-unix-socket", q . maybe mempty pack . configServerUnixSocket)
|
,("server-unix-socket", q . maybe mempty T.pack . configServerUnixSocket)
|
||||||
,("server-unix-socket-mode", q . pack . showSocketMode)
|
,("server-unix-socket-mode", q . T.pack . showSocketMode)
|
||||||
]
|
]
|
||||||
|
|
||||||
-- quote all app.settings
|
-- quote all app.settings
|
||||||
appSettings = second q <$> configAppSettings conf
|
appSettings = second q <$> configAppSettings conf
|
||||||
|
|
||||||
-- quote strings and replace " with \"
|
-- quote strings and replace " with \"
|
||||||
q s = "\"" <> replace "\"" "\\\"" s <> "\""
|
q s = "\"" <> T.replace "\"" "\\\"" s <> "\""
|
||||||
|
|
||||||
showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of
|
showTxEnd c = case (configDbTxRollbackAll c, configDbTxAllowOverride c) of
|
||||||
( False, False ) -> "commit"
|
( False, False ) -> "commit"
|
||||||
@@ -384,7 +395,7 @@ readAppConfig dbSettings env optPath dbUriFile secretFile = do
|
|||||||
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
|
<*> (maybe [] (fmap encodeUtf8 . splitOnCommas) <$> optValue "raw-media-types")
|
||||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||||
<*> (fmap unpack <$> optString "server-unix-socket")
|
<*> (fmap T.unpack <$> optString "server-unix-socket")
|
||||||
<*> parseSocketFileMode "server-unix-socket-mode"
|
<*> parseSocketFileMode "server-unix-socket-mode"
|
||||||
|
|
||||||
parseDbUri :: C.Key -> C.Parser C.Config Text
|
parseDbUri :: C.Key -> C.Parser C.Config Text
|
||||||
@@ -397,11 +408,11 @@ readAppConfig dbSettings env optPath dbUriFile secretFile = do
|
|||||||
let secStr = encodeUtf8 sec
|
let secStr = encodeUtf8 sec
|
||||||
secFile = fromMaybe secStr secretFile
|
secFile = fromMaybe secStr secretFile
|
||||||
-- replace because the JWT is actually base64url encoded which must be turned into just base64 before decoding.
|
-- replace because the JWT is actually base64url encoded which must be turned into just base64 before decoding.
|
||||||
replaceUrlChars = replace "_" "/" . replace "-" "+" . replace "." "="
|
replaceUrlChars = T.replace "_" "/" . T.replace "-" "+" . T.replace "." "="
|
||||||
willBeFile = isPrefixOf "@" (toS secStr) && isNothing secretFile
|
willBeFile = isPrefixOf "@" (toS secStr) && isNothing secretFile
|
||||||
in
|
in
|
||||||
if isB64 && not willBeFile -- don't decode in bas64 if the secret will be a file or it will err. The secFile will be filled with the file contents in a later stage.
|
if isB64 && not willBeFile -- don't decode in bas64 if the secret will be a file or it will err. The secFile will be filled with the file contents in a later stage.
|
||||||
then case B64.decode $ encodeUtf8 $ strip $ replaceUrlChars $ decodeUtf8 secFile of
|
then case B64.decode . encodeUtf8 . T.strip . replaceUrlChars $ decodeUtf8 secFile of
|
||||||
Left errMsg -> fail errMsg
|
Left errMsg -> fail errMsg
|
||||||
Right bs -> pure $ Just bs
|
Right bs -> pure $ Just bs
|
||||||
else pure $ Just secFile
|
else pure $ Just secFile
|
||||||
@@ -411,14 +422,14 @@ readAppConfig dbSettings env optPath dbUriFile secretFile = do
|
|||||||
where
|
where
|
||||||
addFromEnv f = M.toList $ M.union fromEnv $ M.fromList f
|
addFromEnv f = M.toList $ M.union fromEnv $ M.fromList f
|
||||||
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
|
fromEnv = M.mapKeys fromJust $ M.filterWithKey (\k _ -> isJust k) $ M.mapKeys normalize env
|
||||||
normalize k = ("app.settings." <>) <$> stripPrefix "PGRST_APP_SETTINGS_" (toS k)
|
normalize k = ("app.settings." <>) <$> T.stripPrefix "PGRST_APP_SETTINGS_" (toS k)
|
||||||
|
|
||||||
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
parseSocketFileMode :: C.Key -> C.Parser C.Config FileMode
|
||||||
parseSocketFileMode k =
|
parseSocketFileMode k =
|
||||||
optString k >>= \case
|
optString k >>= \case
|
||||||
Nothing -> pure 432 -- return default 660 mode if no value was provided
|
Nothing -> pure 432 -- return default 660 mode if no value was provided
|
||||||
Just fileModeText ->
|
Just fileModeText ->
|
||||||
case (readOct . unpack) fileModeText of
|
case readOct $ T.unpack fileModeText of
|
||||||
[] ->
|
[] ->
|
||||||
fail "Invalid server-unix-socket-mode: not an octal"
|
fail "Invalid server-unix-socket-mode: not an octal"
|
||||||
(fileMode, _):_ ->
|
(fileMode, _):_ ->
|
||||||
@@ -437,7 +448,7 @@ readAppConfig dbSettings env optPath dbUriFile secretFile = do
|
|||||||
parseJwtAudience k =
|
parseJwtAudience k =
|
||||||
optString k >>= \case
|
optString k >>= \case
|
||||||
Nothing -> pure Nothing -- no audience in config file
|
Nothing -> pure Nothing -- no audience in config file
|
||||||
Just aud -> case preview stringOrUri (unpack aud) of
|
Just aud -> case preview stringOrUri (T.unpack aud) of
|
||||||
Nothing -> fail "Invalid Jwt audience. Check your configuration."
|
Nothing -> fail "Invalid Jwt audience. Check your configuration."
|
||||||
aud' -> pure aud'
|
aud' -> pure aud'
|
||||||
|
|
||||||
@@ -510,7 +521,7 @@ readAppConfig dbSettings env optPath dbUriFile secretFile = do
|
|||||||
dashToUnderscore c = c
|
dashToUnderscore c = c
|
||||||
envVarName = "PGRST_" <> (toUpper . dashToUnderscore <$> toS key)
|
envVarName = "PGRST_" <> (toUpper . dashToUnderscore <$> toS key)
|
||||||
reloadableDbSetting =
|
reloadableDbSetting =
|
||||||
let dbSettingName = pack $ dashToUnderscore <$> toS key in
|
let dbSettingName = T.pack $ dashToUnderscore <$> toS key in
|
||||||
if dbSettingName `notElem` [
|
if dbSettingName `notElem` [
|
||||||
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "log_level",
|
"server_host", "server_port", "server_unix_socket", "server_unix_socket_mode", "log_level",
|
||||||
"db_anon_role", "db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_timeout", "db_config"]
|
"db_anon_role", "db_uri", "db_channel_enabled", "db_channel", "db_pool", "db_pool_timeout", "db_config"]
|
||||||
@@ -530,14 +541,14 @@ readAppConfig dbSettings env optPath dbUriFile secretFile = do
|
|||||||
coerceBool (C.Bool b) = Just b
|
coerceBool (C.Bool b) = Just b
|
||||||
coerceBool (C.String s) =
|
coerceBool (C.String s) =
|
||||||
-- parse all kinds of text: True, true, TRUE, "true", ...
|
-- parse all kinds of text: True, true, TRUE, "true", ...
|
||||||
case readMaybe . toS $ toTitle $ filter isAlpha $ toS s of
|
case readMaybe . toS $ T.toTitle $ T.filter isAlpha $ toS s of
|
||||||
Just b -> Just b
|
Just b -> Just b
|
||||||
-- numeric instead?
|
-- numeric instead?
|
||||||
Nothing -> (> 0) <$> (readMaybe $ toS s :: Maybe Integer)
|
Nothing -> (> 0) <$> (readMaybe $ toS s :: Maybe Integer)
|
||||||
coerceBool _ = Nothing
|
coerceBool _ = Nothing
|
||||||
|
|
||||||
splitOnCommas :: C.Value -> [Text]
|
splitOnCommas :: C.Value -> [Text]
|
||||||
splitOnCommas (C.String s) = strip <$> splitOn "," s
|
splitOnCommas (C.String s) = T.strip <$> T.splitOn "," s
|
||||||
splitOnCommas _ = []
|
splitOnCommas _ = []
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
@@ -562,13 +573,13 @@ type Environment = M.Map [Char] Text
|
|||||||
readEnvironment :: IO Environment
|
readEnvironment :: IO Environment
|
||||||
readEnvironment = getEnvironment <&> pgrst
|
readEnvironment = getEnvironment <&> pgrst
|
||||||
where
|
where
|
||||||
pgrst env = M.filterWithKey (\k _ -> "PGRST_" `isPrefixOf` k) $ M.map pack $ M.fromList env
|
pgrst env = M.filterWithKey (\k _ -> "PGRST_" `isPrefixOf` k) $ M.map T.pack $ M.fromList env
|
||||||
|
|
||||||
-- | Read the JWT secret from a file if configJwtSecret is actually a filepath(has @ as its prefix).
|
-- | Read the JWT secret from a file if configJwtSecret is actually a filepath(has @ as its prefix).
|
||||||
-- | To check if the JWT secret is provided is in fact a file path, it must be decoded as 'Text' to be processed.
|
-- | To check if the JWT secret is provided is in fact a file path, it must be decoded as 'Text' to be processed.
|
||||||
readSecretFile :: Maybe B.ByteString -> IO (Maybe B.ByteString)
|
readSecretFile :: Maybe B.ByteString -> IO (Maybe B.ByteString)
|
||||||
readSecretFile mSecret =
|
readSecretFile mSecret =
|
||||||
case (stripPrefix "@" . decodeUtf8) =<< mSecret of
|
case (T.stripPrefix "@" . decodeUtf8) =<< mSecret of
|
||||||
Nothing -> return Nothing
|
Nothing -> return Nothing
|
||||||
Just filename -> Just . chomp <$> BS.readFile (toS filename)
|
Just filename -> Just . chomp <$> BS.readFile (toS filename)
|
||||||
where
|
where
|
||||||
@@ -576,6 +587,6 @@ readSecretFile mSecret =
|
|||||||
|
|
||||||
-- | Read database uri from a separate file if `db-uri` is a filepath.
|
-- | Read database uri from a separate file if `db-uri` is a filepath.
|
||||||
readDbUriFile :: Text -> IO (Maybe Text)
|
readDbUriFile :: Text -> IO (Maybe Text)
|
||||||
readDbUriFile dbUri = case stripPrefix "@" dbUri of
|
readDbUriFile dbUri = case T.stripPrefix "@" dbUri of
|
||||||
Nothing -> return Nothing
|
Nothing -> return Nothing
|
||||||
Just filename -> Just . strip <$> readFile (toS filename)
|
Just filename -> Just . T.strip <$> readFile (toS filename)
|
||||||
|
|||||||
@@ -0,0 +1,59 @@
|
|||||||
|
{-|
|
||||||
|
Module : PostgREST.Types
|
||||||
|
Description : PostgREST common types and functions used by the rest of the modules
|
||||||
|
-}
|
||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
|
||||||
|
module PostgREST.Config.JSPath
|
||||||
|
( JSPath
|
||||||
|
, JSPathExp(..)
|
||||||
|
, pRoleClaimKey
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Text.ParserCombinators.Parsec as P
|
||||||
|
|
||||||
|
import Data.Either.Combinators (mapLeft)
|
||||||
|
import Text.ParserCombinators.Parsec ((<?>))
|
||||||
|
import Text.Read (read)
|
||||||
|
|
||||||
|
import qualified GHC.Show (show)
|
||||||
|
|
||||||
|
import Protolude hiding (toS)
|
||||||
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
|
||||||
|
-- | full jspath, e.g. .property[0].attr.detail
|
||||||
|
type JSPath = [JSPathExp]
|
||||||
|
|
||||||
|
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
|
||||||
|
data JSPathExp
|
||||||
|
= JSPKey Text
|
||||||
|
| JSPIdx Int
|
||||||
|
|
||||||
|
instance Show JSPathExp where
|
||||||
|
-- TODO: this needs to be quoted properly for special chars
|
||||||
|
show (JSPKey k) = "." <> show k
|
||||||
|
show (JSPIdx i) = "[" <> show i <> "]"
|
||||||
|
|
||||||
|
-- Used for the config value "role-claim-key"
|
||||||
|
pRoleClaimKey :: Text -> Either Text JSPath
|
||||||
|
pRoleClaimKey selStr =
|
||||||
|
mapLeft show $ P.parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
|
||||||
|
|
||||||
|
pJSPath :: P.Parser JSPath
|
||||||
|
pJSPath = toJSPath <$> (period *> pPath `P.sepBy` period <* P.eof)
|
||||||
|
where
|
||||||
|
toJSPath :: [(Text, Maybe Int)] -> JSPath
|
||||||
|
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
|
||||||
|
period = P.char '.' <?> "period (.)"
|
||||||
|
pPath :: P.Parser (Text, Maybe Int)
|
||||||
|
pPath = (,) <$> pJSPKey <*> P.optionMaybe pJSPIdx
|
||||||
|
|
||||||
|
pJSPKey :: P.Parser Text
|
||||||
|
pJSPKey = toS <$> P.many1 (P.alphaNum <|> P.oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
|
||||||
|
|
||||||
|
pJSPIdx :: P.Parser Int
|
||||||
|
pJSPIdx = P.char '[' *> (read <$> P.many1 P.digit) <* P.char ']' <?> "array index [0..n]"
|
||||||
|
|
||||||
|
pQuotedValue :: P.Parser Text
|
||||||
|
pQuotedValue = toS <$> (P.char '"' *> P.many (P.noneOf "\"") <* P.char '"')
|
||||||
@@ -3,8 +3,9 @@
|
|||||||
Module : PostgREST.Private.ProxyUri
|
Module : PostgREST.Private.ProxyUri
|
||||||
Description : Proxy Uri validator
|
Description : Proxy Uri validator
|
||||||
-}
|
-}
|
||||||
module PostgREST.Private.ProxyUri (
|
module PostgREST.Config.Proxy
|
||||||
isMalformedProxyUri
|
( Proxy(..)
|
||||||
|
, isMalformedProxyUri
|
||||||
, toURI
|
, toURI
|
||||||
) where
|
) where
|
||||||
|
|
||||||
@@ -16,6 +17,13 @@ import Protolude hiding (Proxy, dropWhile, get, intercalate,
|
|||||||
toLower, toS, (&))
|
toLower, toS, (&))
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
data Proxy = Proxy
|
||||||
|
{ proxyScheme :: Text
|
||||||
|
, proxyHost :: Text
|
||||||
|
, proxyPort :: Integer
|
||||||
|
, proxyPath :: Text
|
||||||
|
}
|
||||||
|
|
||||||
{-|
|
{-|
|
||||||
Test whether a proxy uri is malformed or not.
|
Test whether a proxy uri is malformed or not.
|
||||||
A valid proxy uri should be an absolute uri without query and user info,
|
A valid proxy uri should be an absolute uri without query and user info,
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
|
||||||
|
module PostgREST.ContentType
|
||||||
|
( ContentType(..)
|
||||||
|
, toHeader
|
||||||
|
, toMime
|
||||||
|
, decodeContentType
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString as BS
|
||||||
|
import qualified Data.ByteString.Internal as BS (c2w)
|
||||||
|
|
||||||
|
import Network.HTTP.Types.Header (Header, hContentType)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
-- | Enumeration of currently supported response content types
|
||||||
|
data ContentType
|
||||||
|
= CTApplicationJSON
|
||||||
|
| CTSingularJSON
|
||||||
|
| CTTextCSV
|
||||||
|
| CTTextPlain
|
||||||
|
| CTOpenAPI
|
||||||
|
| CTUrlEncoded
|
||||||
|
| CTOctetStream
|
||||||
|
| CTAny
|
||||||
|
| CTOther ByteString
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
-- | Convert from ContentType to a full HTTP Header
|
||||||
|
toHeader :: ContentType -> Header
|
||||||
|
toHeader ct = (hContentType, toMime ct <> charset)
|
||||||
|
where
|
||||||
|
charset = case ct of
|
||||||
|
CTOctetStream -> mempty
|
||||||
|
CTOther _ -> mempty
|
||||||
|
_ -> "; charset=utf-8"
|
||||||
|
|
||||||
|
-- | Convert from ContentType to a ByteString representing the mime type
|
||||||
|
toMime :: ContentType -> ByteString
|
||||||
|
toMime CTApplicationJSON = "application/json"
|
||||||
|
toMime CTTextCSV = "text/csv"
|
||||||
|
toMime CTTextPlain = "text/plain"
|
||||||
|
toMime CTOpenAPI = "application/openapi+json"
|
||||||
|
toMime CTSingularJSON = "application/vnd.pgrst.object+json"
|
||||||
|
toMime CTUrlEncoded = "application/x-www-form-urlencoded"
|
||||||
|
toMime CTOctetStream = "application/octet-stream"
|
||||||
|
toMime CTAny = "*/*"
|
||||||
|
toMime (CTOther ct) = ct
|
||||||
|
|
||||||
|
-- | Convert from ByteString to ContentType. Warning: discards MIME parameters
|
||||||
|
decodeContentType :: BS.ByteString -> ContentType
|
||||||
|
decodeContentType ct =
|
||||||
|
case BS.takeWhile (/= BS.c2w ';') ct of
|
||||||
|
"application/json" -> CTApplicationJSON
|
||||||
|
"text/csv" -> CTTextCSV
|
||||||
|
"text/plain" -> CTTextPlain
|
||||||
|
"application/openapi+json" -> CTOpenAPI
|
||||||
|
"application/vnd.pgrst.object+json" -> CTSingularJSON
|
||||||
|
"application/vnd.pgrst.object" -> CTSingularJSON
|
||||||
|
"application/x-www-form-urlencoded" -> CTUrlEncoded
|
||||||
|
"application/octet-stream" -> CTOctetStream
|
||||||
|
"*/*" -> CTAny
|
||||||
|
ct' -> CTOther ct'
|
||||||
@@ -8,6 +8,8 @@ The schema cache is necessary for resource embedding, foreign keys are used for
|
|||||||
|
|
||||||
These queries are executed once at startup or when PostgREST is reloaded.
|
These queries are executed once at startup or when PostgREST is reloaded.
|
||||||
-}
|
-}
|
||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
{-# LANGUAGE MultiParamTypeClasses #-}
|
{-# LANGUAGE MultiParamTypeClasses #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
@@ -16,14 +18,18 @@ These queries are executed once at startup or when PostgREST is reloaded.
|
|||||||
{-# LANGUAGE ScopedTypeVariables #-}
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
{-# LANGUAGE TypeSynonymInstances #-}
|
{-# LANGUAGE TypeSynonymInstances #-}
|
||||||
|
|
||||||
module PostgREST.DbStructure (
|
module PostgREST.DbStructure
|
||||||
getDbStructure
|
( DbStructure(..)
|
||||||
|
, getDbStructure
|
||||||
, accessibleTables
|
, accessibleTables
|
||||||
, accessibleProcs
|
, accessibleProcs
|
||||||
, schemaDescription
|
, schemaDescription
|
||||||
, getPgVersion
|
, getPgVersion
|
||||||
|
, tableCols
|
||||||
|
, tablePKCols
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
@@ -35,13 +41,52 @@ import qualified Hasql.Transaction as HT
|
|||||||
import Contravariant.Extras (contrazip2)
|
import Contravariant.Extras (contrazip2)
|
||||||
import Data.Set as S (fromList)
|
import Data.Set as S (fromList)
|
||||||
import Data.Text (split)
|
import Data.Text (split)
|
||||||
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Identifiers (QualifiedIdentifier (..),
|
||||||
|
Schema, TableName)
|
||||||
|
import PostgREST.DbStructure.PgVersion (PgVersion (..))
|
||||||
|
import PostgREST.DbStructure.Proc (PgArg (..), PgType (..),
|
||||||
|
ProcDescription (..),
|
||||||
|
ProcVolatility (..),
|
||||||
|
ProcsMap, RetType (..))
|
||||||
|
import PostgREST.DbStructure.Relation (Cardinality (..),
|
||||||
|
ForeignKey (..), Link (..),
|
||||||
|
PrimaryKey (..),
|
||||||
|
Relation (..))
|
||||||
|
import PostgREST.DbStructure.Table (Column (..), Table (..))
|
||||||
|
|
||||||
import Protolude hiding (toS)
|
import Protolude hiding (toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
import Protolude.Unsafe (unsafeHead)
|
import Protolude.Unsafe (unsafeHead)
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
|
||||||
|
|
||||||
import PostgREST.Private.Common
|
|
||||||
import PostgREST.Types
|
data DbStructure = DbStructure
|
||||||
|
{ dbTables :: [Table]
|
||||||
|
, dbColumns :: [Column]
|
||||||
|
, dbRelations :: [Relation]
|
||||||
|
, dbPrimaryKeys :: [PrimaryKey]
|
||||||
|
, dbProcs :: ProcsMap
|
||||||
|
, pgVersion :: PgVersion
|
||||||
|
}
|
||||||
|
deriving (Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
-- TODO Table could hold references to all its Columns
|
||||||
|
tableCols :: DbStructure -> Schema -> TableName -> [Column]
|
||||||
|
tableCols dbs tSchema tName = filter (\Column{colTable=Table{tableSchema=s, tableName=t}} -> s==tSchema && t==tName) $ dbColumns dbs
|
||||||
|
|
||||||
|
-- TODO Table could hold references to all its PrimaryKeys
|
||||||
|
tablePKCols :: DbStructure -> Schema -> TableName -> [Text]
|
||||||
|
tablePKCols dbs tSchema tName = pkName <$> filter (\pk -> tSchema == (tableSchema . pkTable) pk && tName == (tableName . pkTable) pk) (dbPrimaryKeys dbs)
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
-- | The source table column a view column refers to
|
||||||
|
type SourceColumn = (Column, ViewColumn)
|
||||||
|
type ViewColumn = Column
|
||||||
|
|
||||||
|
-- | A SQL query that can be executed independently
|
||||||
|
type SqlQuery = ByteString
|
||||||
|
|
||||||
getDbStructure :: [Schema] -> [Schema] -> PgVersion -> Bool -> HT.Transaction DbStructure
|
getDbStructure :: [Schema] -> [Schema] -> PgVersion -> Bool -> HT.Transaction DbStructure
|
||||||
getDbStructure schemas extraSearchPath pgVer prepared = do
|
getDbStructure schemas extraSearchPath pgVer prepared = do
|
||||||
@@ -839,3 +884,24 @@ getPgVersion = H.statement mempty $ H.Statement sql HE.noParams versionRow False
|
|||||||
where
|
where
|
||||||
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
|
||||||
versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text
|
versionRow = HD.singleRow $ PgVersion <$> column HD.int4 <*> column HD.text
|
||||||
|
|
||||||
|
param :: HE.Value a -> HE.Params a
|
||||||
|
param = HE.param . HE.nonNullable
|
||||||
|
|
||||||
|
arrayParam :: HE.Value a -> HE.Params [a]
|
||||||
|
arrayParam = param . HE.foldableArray . HE.nonNullable
|
||||||
|
|
||||||
|
compositeArrayColumn :: HD.Composite a -> HD.Row [a]
|
||||||
|
compositeArrayColumn = arrayColumn . HD.composite
|
||||||
|
|
||||||
|
compositeField :: HD.Value a -> HD.Composite a
|
||||||
|
compositeField = HD.field . HD.nonNullable
|
||||||
|
|
||||||
|
column :: HD.Value a -> HD.Row a
|
||||||
|
column = HD.column . HD.nonNullable
|
||||||
|
|
||||||
|
nullableColumn :: HD.Value a -> HD.Row (Maybe a)
|
||||||
|
nullableColumn = HD.column . HD.nullable
|
||||||
|
|
||||||
|
arrayColumn :: HD.Value a -> HD.Row [a]
|
||||||
|
arrayColumn = column . HD.listArray . HD.nonNullable
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
|
module PostgREST.DbStructure.Identifiers
|
||||||
|
( QualifiedIdentifier(..)
|
||||||
|
, Schema
|
||||||
|
, TableName
|
||||||
|
, FieldName
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
-- | Represents a pg identifier with a prepended schema name "schema.table".
|
||||||
|
-- When qiSchema is "", the schema is defined by the pg search_path.
|
||||||
|
data QualifiedIdentifier = QualifiedIdentifier
|
||||||
|
{ qiSchema :: Schema
|
||||||
|
, qiName :: TableName
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey)
|
||||||
|
|
||||||
|
instance Hashable QualifiedIdentifier
|
||||||
|
|
||||||
|
type Schema = Text
|
||||||
|
type TableName = Text
|
||||||
|
type FieldName = Text
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
module PostgREST.DbStructure.PgVersion
|
||||||
|
( PgVersion(..)
|
||||||
|
, minimumPgVersion
|
||||||
|
, pgVersion95
|
||||||
|
, pgVersion96
|
||||||
|
, pgVersion100
|
||||||
|
, pgVersion109
|
||||||
|
, pgVersion110
|
||||||
|
, pgVersion112
|
||||||
|
, pgVersion114
|
||||||
|
, pgVersion121
|
||||||
|
, pgVersion130
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
data PgVersion = PgVersion
|
||||||
|
{ pgvNum :: Int32
|
||||||
|
, pgvName :: Text
|
||||||
|
}
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
instance Ord PgVersion where
|
||||||
|
(PgVersion v1 _) `compare` (PgVersion v2 _) = v1 `compare` v2
|
||||||
|
|
||||||
|
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
||||||
|
minimumPgVersion :: PgVersion
|
||||||
|
minimumPgVersion = pgVersion95
|
||||||
|
|
||||||
|
pgVersion95 :: PgVersion
|
||||||
|
pgVersion95 = PgVersion 90500 "9.5"
|
||||||
|
|
||||||
|
pgVersion96 :: PgVersion
|
||||||
|
pgVersion96 = PgVersion 90600 "9.6"
|
||||||
|
|
||||||
|
pgVersion100 :: PgVersion
|
||||||
|
pgVersion100 = PgVersion 100000 "10"
|
||||||
|
|
||||||
|
pgVersion109 :: PgVersion
|
||||||
|
pgVersion109 = PgVersion 100009 "10.9"
|
||||||
|
|
||||||
|
pgVersion110 :: PgVersion
|
||||||
|
pgVersion110 = PgVersion 110000 "11.0"
|
||||||
|
|
||||||
|
pgVersion112 :: PgVersion
|
||||||
|
pgVersion112 = PgVersion 110002 "11.2"
|
||||||
|
|
||||||
|
pgVersion114 :: PgVersion
|
||||||
|
pgVersion114 = PgVersion 110004 "11.4"
|
||||||
|
|
||||||
|
pgVersion121 :: PgVersion
|
||||||
|
pgVersion121 = PgVersion 120001 "12.1"
|
||||||
|
|
||||||
|
pgVersion130 :: PgVersion
|
||||||
|
pgVersion130 = PgVersion 130000 "13.0"
|
||||||
@@ -0,0 +1,120 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
|
module PostgREST.DbStructure.Proc
|
||||||
|
( PgArg(..)
|
||||||
|
, PgType(..)
|
||||||
|
, ProcDescription(..)
|
||||||
|
, ProcVolatility(..)
|
||||||
|
, ProcsMap
|
||||||
|
, RetType(..)
|
||||||
|
, findProc
|
||||||
|
, procReturnsScalar
|
||||||
|
, procReturnsSingle
|
||||||
|
, procTableName
|
||||||
|
, specifiedProcArgs
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
|
import qualified Data.Set as S
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier (..),
|
||||||
|
Schema, TableName)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
data PgArg = PgArg
|
||||||
|
{ pgaName :: Text
|
||||||
|
, pgaType :: Text
|
||||||
|
, pgaReq :: Bool
|
||||||
|
, pgaVar :: Bool
|
||||||
|
}
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data PgType
|
||||||
|
= Scalar
|
||||||
|
| Composite QualifiedIdentifier
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data RetType
|
||||||
|
= Single PgType
|
||||||
|
| SetOf PgType
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data ProcVolatility
|
||||||
|
= Volatile
|
||||||
|
| Stable
|
||||||
|
| Immutable
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data ProcDescription = ProcDescription
|
||||||
|
{ pdSchema :: Schema
|
||||||
|
, pdName :: Text
|
||||||
|
, pdDescription :: Maybe Text
|
||||||
|
, pdArgs :: [PgArg]
|
||||||
|
, pdReturnType :: RetType
|
||||||
|
, pdVolatility :: ProcVolatility
|
||||||
|
, pdHasVariadic :: Bool
|
||||||
|
}
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
-- Order by least number of args in the case of overloaded functions
|
||||||
|
instance Ord ProcDescription where
|
||||||
|
ProcDescription schema1 name1 des1 args1 rt1 vol1 hasVar1 `compare` ProcDescription schema2 name2 des2 args2 rt2 vol2 hasVar2
|
||||||
|
| schema1 == schema2 && name1 == name2 && length args1 < length args2 = LT
|
||||||
|
| schema2 == schema2 && name1 == name2 && length args1 > length args2 = GT
|
||||||
|
| otherwise = (schema1, name1, des1, args1, rt1, vol1, hasVar1) `compare` (schema2, name2, des2, args2, rt2, vol2, hasVar2)
|
||||||
|
|
||||||
|
-- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription).
|
||||||
|
-- | It uses a HashMap for a faster lookup.
|
||||||
|
type ProcsMap = M.HashMap QualifiedIdentifier [ProcDescription]
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Search a pg procedure by its parameters. Since a function can be overloaded, the name is not enough to find it.
|
||||||
|
An overloaded function can have a different volatility or even a different return type.
|
||||||
|
Ideally, handling overloaded functions should be left to pg itself. But we need to know certain proc attributes in advance.
|
||||||
|
-}
|
||||||
|
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ProcDescription
|
||||||
|
findProc qi payloadKeys paramsAsSingleObject allProcs = fromMaybe fallback bestMatch
|
||||||
|
where
|
||||||
|
-- instead of passing Maybe ProcDescription around, we create a fallback description here when we can't find a matching function
|
||||||
|
-- args is empty, but because "specifiedProcArgs" will fill the missing arguments with default type text, this is not a problem
|
||||||
|
fallback = ProcDescription (qiSchema qi) (qiName qi) Nothing mempty (SetOf $ Composite $ QualifiedIdentifier mempty "record") Volatile False
|
||||||
|
bestMatch =
|
||||||
|
case M.lookup qi allProcs of
|
||||||
|
Nothing -> Nothing
|
||||||
|
Just [proc] -> Just proc -- if it's not an overloaded function then immediately get the ProcDescription
|
||||||
|
Just procs -> find matches procs -- Handle overloaded functions case
|
||||||
|
matches proc =
|
||||||
|
if paramsAsSingleObject
|
||||||
|
-- if the arg is not of json type let the db give the err
|
||||||
|
then length (pdArgs proc) == 1
|
||||||
|
else payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> pdArgs proc)
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Search the procedure parameters by matching them with the specified keys.
|
||||||
|
If the key doesn't match a parameter, a parameter with a default type "text" is assumed.
|
||||||
|
-}
|
||||||
|
specifiedProcArgs :: S.Set FieldName -> ProcDescription -> [PgArg]
|
||||||
|
specifiedProcArgs keys proc =
|
||||||
|
(\k -> fromMaybe (PgArg k "text" True False) (find ((==) k . pgaName) (pdArgs proc))) <$> S.toList keys
|
||||||
|
|
||||||
|
procReturnsScalar :: ProcDescription -> Bool
|
||||||
|
procReturnsScalar proc = case proc of
|
||||||
|
ProcDescription{pdReturnType = (Single Scalar)} -> True
|
||||||
|
ProcDescription{pdReturnType = (SetOf Scalar)} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
procReturnsSingle :: ProcDescription -> Bool
|
||||||
|
procReturnsSingle proc = case proc of
|
||||||
|
ProcDescription{pdReturnType = (Single _)} -> True
|
||||||
|
_ -> False
|
||||||
|
|
||||||
|
procTableName :: ProcDescription -> Maybe TableName
|
||||||
|
procTableName proc = case pdReturnType proc of
|
||||||
|
SetOf (Composite qi) -> Just $ qiName qi
|
||||||
|
Single (Composite qi) -> Just $ qiName qi
|
||||||
|
_ -> Nothing
|
||||||
@@ -0,0 +1,76 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
|
module PostgREST.DbStructure.Relation
|
||||||
|
( Cardinality(..)
|
||||||
|
, Constraint
|
||||||
|
, ForeignKey(..)
|
||||||
|
, Link(..)
|
||||||
|
, PrimaryKey(..)
|
||||||
|
, Relation(..)
|
||||||
|
, isSelfReference
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Table (Column (..), ForeignKey (..),
|
||||||
|
Table (..))
|
||||||
|
|
||||||
|
import qualified GHC.Show (show)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
-- | "Relation"ship between two tables.
|
||||||
|
--
|
||||||
|
-- The order of the relColumns and relFColumns should be maintained to get the
|
||||||
|
-- join conditions right.
|
||||||
|
--
|
||||||
|
-- TODO merge relColumns and relFColumns to a tuple or Data.Bimap
|
||||||
|
data Relation = Relation
|
||||||
|
{ relTable :: Table
|
||||||
|
, relColumns :: [Column]
|
||||||
|
, relFTable :: Table
|
||||||
|
, relFColumns :: [Column]
|
||||||
|
, relType :: Cardinality
|
||||||
|
, relLink :: Link -- ^ Constraint on O2M/M2O, Junction for M2M Cardinality
|
||||||
|
}
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
type ConstraintName = Text
|
||||||
|
|
||||||
|
-- | Junction table on an M2M relationship
|
||||||
|
data Link
|
||||||
|
= Constraint
|
||||||
|
{ constName :: ConstraintName }
|
||||||
|
| Junction
|
||||||
|
{ junTable :: Table
|
||||||
|
, junLink1 :: Link
|
||||||
|
, junCols1 :: [Column]
|
||||||
|
, junLink2 :: Link
|
||||||
|
, junCols2 :: [Column]
|
||||||
|
}
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data PrimaryKey = PrimaryKey
|
||||||
|
{ pkTable :: Table
|
||||||
|
, pkName :: Text
|
||||||
|
}
|
||||||
|
deriving (Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
-- | The relationship
|
||||||
|
-- [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
|
||||||
|
-- TODO: missing one-to-one
|
||||||
|
data Cardinality
|
||||||
|
= O2M -- ^ one-to-many, previously known as Parent
|
||||||
|
| M2O -- ^ many-to-one, previously known as Child
|
||||||
|
| M2M -- ^ many-to-many, previously known as Many
|
||||||
|
deriving (Eq, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
instance Show Cardinality where
|
||||||
|
show O2M = "o2m"
|
||||||
|
show M2O = "m2o"
|
||||||
|
show M2M = "m2m"
|
||||||
|
|
||||||
|
isSelfReference :: Relation -> Bool
|
||||||
|
isSelfReference r = relTable r == relFTable r
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
{-# LANGUAGE DeriveAnyClass #-}
|
||||||
|
{-# LANGUAGE DeriveGeneric #-}
|
||||||
|
|
||||||
|
module PostgREST.DbStructure.Table
|
||||||
|
( Column(..)
|
||||||
|
, ForeignKey(..)
|
||||||
|
, Table(..)
|
||||||
|
, tableQi
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier (..),
|
||||||
|
Schema, TableName)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
data Table = Table
|
||||||
|
{ tableSchema :: Schema
|
||||||
|
, tableName :: TableName
|
||||||
|
, tableDescription :: Maybe Text
|
||||||
|
, tableInsertable :: Bool
|
||||||
|
}
|
||||||
|
deriving (Show, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
instance Eq Table where
|
||||||
|
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
|
||||||
|
|
||||||
|
tableQi :: Table -> QualifiedIdentifier
|
||||||
|
tableQi Table{tableSchema=s, tableName=n} = QualifiedIdentifier s n
|
||||||
|
|
||||||
|
newtype ForeignKey = ForeignKey
|
||||||
|
{ fkCol :: Column }
|
||||||
|
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
data Column = Column
|
||||||
|
{ colTable :: Table
|
||||||
|
, colName :: FieldName
|
||||||
|
, colDescription :: Maybe Text
|
||||||
|
, colNullable :: Bool
|
||||||
|
, colType :: Text
|
||||||
|
, colMaxLen :: Maybe Int32
|
||||||
|
, colDefault :: Maybe Text
|
||||||
|
, colEnum :: [Text]
|
||||||
|
, colFK :: Maybe ForeignKey
|
||||||
|
}
|
||||||
|
deriving (Ord, Generic, JSON.ToJSON)
|
||||||
|
|
||||||
|
instance Eq Column where
|
||||||
|
Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2
|
||||||
|
|
||||||
|
data PrimaryKey = PrimaryKey
|
||||||
|
{ pkTable :: Table
|
||||||
|
, pkName :: Text
|
||||||
|
}
|
||||||
|
deriving (Generic, JSON.ToJSON)
|
||||||
+17
-12
@@ -5,8 +5,8 @@ Description : PostgREST error HTTP responses
|
|||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module PostgREST.Error (
|
module PostgREST.Error
|
||||||
errorResponseFor
|
( errorResponseFor
|
||||||
, ApiRequestError(..)
|
, ApiRequestError(..)
|
||||||
, PgError(..)
|
, PgError(..)
|
||||||
, Error(..)
|
, Error(..)
|
||||||
@@ -24,9 +24,14 @@ import qualified Network.HTTP.Types.Status as HT
|
|||||||
import Data.Aeson ((.=))
|
import Data.Aeson ((.=))
|
||||||
import Network.Wai (Response, responseLBS)
|
import Network.Wai (Response, responseLBS)
|
||||||
|
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header (Header)
|
||||||
|
|
||||||
|
import PostgREST.ContentType (ContentType (..))
|
||||||
|
import qualified PostgREST.ContentType as ContentType
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Relation (Link (..), Relation (..))
|
||||||
|
import PostgREST.DbStructure.Table (Column (..), Table (..))
|
||||||
|
|
||||||
import PostgREST.Types
|
|
||||||
import Protolude hiding (toS)
|
import Protolude hiding (toS)
|
||||||
import Protolude.Conv (toS, toSL)
|
import Protolude.Conv (toS, toSL)
|
||||||
|
|
||||||
@@ -67,7 +72,7 @@ instance PgrstError ApiRequestError where
|
|||||||
status AmbiguousRelBetween{} = HT.status300
|
status AmbiguousRelBetween{} = HT.status300
|
||||||
status (UnacceptableSchema _) = HT.status406
|
status (UnacceptableSchema _) = HT.status406
|
||||||
|
|
||||||
headers _ = [toHeader CTApplicationJSON]
|
headers _ = [ContentType.toHeader CTApplicationJSON]
|
||||||
|
|
||||||
instance JSON.ToJSON ApiRequestError where
|
instance JSON.ToJSON ApiRequestError where
|
||||||
toJSON (ParseRequestError message details) = JSON.object [
|
toJSON (ParseRequestError message details) = JSON.object [
|
||||||
@@ -120,8 +125,8 @@ instance PgrstError PgError where
|
|||||||
|
|
||||||
headers err =
|
headers err =
|
||||||
if status err == HT.status401
|
if status err == HT.status401
|
||||||
then [toHeader CTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
then [ContentType.toHeader CTApplicationJSON, ("WWW-Authenticate", "Bearer") :: Header]
|
||||||
else [toHeader CTApplicationJSON]
|
else [ContentType.toHeader CTApplicationJSON]
|
||||||
|
|
||||||
instance JSON.ToJSON PgError where
|
instance JSON.ToJSON PgError where
|
||||||
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
toJSON (PgError _ usageError) = JSON.toJSON usageError
|
||||||
@@ -249,11 +254,11 @@ instance PgrstError Error where
|
|||||||
status (PgErr err) = status err
|
status (PgErr err) = status err
|
||||||
status (ApiRequestError err) = status err
|
status (ApiRequestError err) = status err
|
||||||
|
|
||||||
headers (SingularityError _) = [toHeader CTSingularJSON]
|
headers (SingularityError _) = [ContentType.toHeader CTSingularJSON]
|
||||||
headers (JwtTokenInvalid m) = [toHeader CTApplicationJSON, invalidTokenHeader m]
|
headers (JwtTokenInvalid m) = [ContentType.toHeader CTApplicationJSON, invalidTokenHeader m]
|
||||||
headers (PgErr err) = headers err
|
headers (PgErr err) = headers err
|
||||||
headers (ApiRequestError err) = headers err
|
headers (ApiRequestError err) = headers err
|
||||||
headers _ = [toHeader CTApplicationJSON]
|
headers _ = [ContentType.toHeader CTApplicationJSON]
|
||||||
|
|
||||||
instance JSON.ToJSON Error where
|
instance JSON.ToJSON Error where
|
||||||
toJSON GucHeadersError = JSON.object [
|
toJSON GucHeadersError = JSON.object [
|
||||||
@@ -261,7 +266,7 @@ instance JSON.ToJSON Error where
|
|||||||
toJSON GucStatusError = JSON.object [
|
toJSON GucStatusError = JSON.object [
|
||||||
"message" .= ("response.status guc must be a valid status code" :: Text)]
|
"message" .= ("response.status guc must be a valid status code" :: Text)]
|
||||||
toJSON (BinaryFieldError ct) = JSON.object [
|
toJSON (BinaryFieldError ct) = JSON.object [
|
||||||
"message" .= ((toS (toMime ct) <> " requested but more than one column was selected") :: Text)]
|
"message" .= ((toS (ContentType.toMime ct) <> " requested but more than one column was selected") :: Text)]
|
||||||
toJSON ConnectionLostError = JSON.object [
|
toJSON ConnectionLostError = JSON.object [
|
||||||
"message" .= ("Database connection lost. Retrying the connection." :: Text)]
|
"message" .= ("Database connection lost. Retrying the connection." :: Text)]
|
||||||
|
|
||||||
@@ -274,7 +279,7 @@ instance JSON.ToJSON Error where
|
|||||||
"message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)]
|
"message" .= ("None of these Content-Types are available: " <> (toS . intercalate ", " . map toS) cts :: Text)]
|
||||||
toJSON (SingularityError n) = JSON.object [
|
toJSON (SingularityError n) = JSON.object [
|
||||||
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
"message" .= ("JSON object requested, multiple (or no) rows returned" :: Text),
|
||||||
"details" .= T.unwords ["Results contain", show n, "rows,", toS (toMime CTSingularJSON), "requires 1 row"]]
|
"details" .= T.unwords ["Results contain", show n, "rows,", toS (ContentType.toMime CTSingularJSON), "requires 1 row"]]
|
||||||
|
|
||||||
toJSON JwtTokenMissing = JSON.object [
|
toJSON JwtTokenMissing = JSON.object [
|
||||||
"message" .= ("Server lacks JWT secret" :: Text)]
|
"message" .= ("Server lacks JWT secret" :: Text)]
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
module PostgREST.GucHeader
|
||||||
|
( GucHeader
|
||||||
|
, unwrapGucHeader
|
||||||
|
, addHeadersIfNotIncluded
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import qualified Data.HashMap.Strict as M
|
||||||
|
|
||||||
|
import Network.HTTP.Types.Header (Header)
|
||||||
|
|
||||||
|
import Protolude hiding (toS)
|
||||||
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
|
||||||
|
{-|
|
||||||
|
Custom guc header, it's obtained by parsing the json in a:
|
||||||
|
`SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]'
|
||||||
|
-}
|
||||||
|
newtype GucHeader = GucHeader (CI.CI ByteString, ByteString)
|
||||||
|
|
||||||
|
instance JSON.FromJSON GucHeader where
|
||||||
|
parseJSON (JSON.Object o) = case headMay (M.toList o) of
|
||||||
|
Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (CI.mk $ toS k, toS s)
|
||||||
|
| otherwise -> mzero
|
||||||
|
_ -> mzero
|
||||||
|
parseJSON _ = mzero
|
||||||
|
|
||||||
|
unwrapGucHeader :: GucHeader -> Header
|
||||||
|
unwrapGucHeader (GucHeader (k, v)) = (k, v)
|
||||||
|
|
||||||
|
-- | Add headers not already included to allow the user to override them instead of duplicating them
|
||||||
|
addHeadersIfNotIncluded :: [Header] -> [Header] -> [Header]
|
||||||
|
addHeadersIfNotIncluded newHeaders initialHeaders =
|
||||||
|
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
|
||||||
|
initialHeaders
|
||||||
+59
-55
@@ -12,50 +12,49 @@ module PostgREST.Middleware
|
|||||||
, optionalRollback
|
, optionalRollback
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Hasql.Decoders as HD
|
|
||||||
import qualified Hasql.DynamicStatements.Statement as H
|
|
||||||
import PostgREST.Private.Common
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import qualified Data.CaseInsensitive as CI
|
import qualified Data.CaseInsensitive as CI
|
||||||
import Data.Function (id)
|
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import Data.List (lookup)
|
|
||||||
import Data.Scientific (FPFormat (..),
|
|
||||||
formatScientific,
|
|
||||||
isInteger)
|
|
||||||
import qualified Data.Text as T
|
import qualified Data.Text as T
|
||||||
|
import qualified Hasql.Decoders as HD
|
||||||
|
import qualified Hasql.DynamicStatements.Snippet as H hiding
|
||||||
|
(sql)
|
||||||
|
import qualified Hasql.DynamicStatements.Statement as H
|
||||||
import qualified Hasql.Transaction as H
|
import qualified Hasql.Transaction as H
|
||||||
import qualified Network.HTTP.Types.Header as HTTP
|
import qualified Network.HTTP.Types.Header as HTTP
|
||||||
import Network.HTTP.Types.Status (Status, status400,
|
|
||||||
status500, statusCode)
|
|
||||||
import qualified Network.Wai as Wai
|
import qualified Network.Wai as Wai
|
||||||
import Network.Wai.Logger (showSockAddr)
|
import qualified Network.Wai.Logger as Wai
|
||||||
|
import qualified Network.Wai.Middleware.Cors as Wai
|
||||||
|
import qualified Network.Wai.Middleware.Gzip as Wai
|
||||||
|
import qualified Network.Wai.Middleware.RequestLogger as Wai
|
||||||
|
import qualified Network.Wai.Middleware.Static as Wai
|
||||||
|
|
||||||
|
import Data.Function (id)
|
||||||
|
import Data.List (lookup)
|
||||||
|
import Data.Scientific (FPFormat (..), formatScientific,
|
||||||
|
isInteger)
|
||||||
|
import Network.HTTP.Types.Status (Status, status400, status500,
|
||||||
|
statusCode)
|
||||||
|
import System.IO.Unsafe (unsafePerformIO)
|
||||||
import System.Log.FastLogger (toLogStr)
|
import System.Log.FastLogger (toLogStr)
|
||||||
|
|
||||||
import Network.Wai
|
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||||
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..),
|
|
||||||
cors)
|
|
||||||
import Network.Wai.Middleware.Gzip (def, gzip)
|
|
||||||
import Network.Wai.Middleware.RequestLogger
|
|
||||||
import Network.Wai.Middleware.Static (only, staticPolicy)
|
|
||||||
|
|
||||||
import qualified PostgREST.Types as Types
|
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ApiRequest (..))
|
|
||||||
import PostgREST.Config (AppConfig (..))
|
|
||||||
import PostgREST.Error (Error, errorResponseFor)
|
import PostgREST.Error (Error, errorResponseFor)
|
||||||
import PostgREST.QueryBuilder (setConfigLocal)
|
import PostgREST.GucHeader (addHeadersIfNotIncluded)
|
||||||
import PostgREST.Types (LogLevel (..))
|
import PostgREST.Query.SqlFragment (intercalateSnippet,
|
||||||
|
unknownLiteral)
|
||||||
|
import PostgREST.Request.ApiRequest (ApiRequest (..))
|
||||||
|
|
||||||
|
import PostgREST.Request.Preferences
|
||||||
|
|
||||||
import Protolude hiding (head, toS)
|
import Protolude hiding (head, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
import System.IO.Unsafe (unsafePerformIO)
|
|
||||||
|
|
||||||
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
-- | Runs local(transaction scoped) GUCs for every request, plus the pre-request function
|
||||||
runPgLocals :: AppConfig -> M.HashMap Text JSON.Value ->
|
runPgLocals :: AppConfig -> M.HashMap Text JSON.Value ->
|
||||||
(ApiRequest -> ExceptT Error H.Transaction Response) ->
|
(ApiRequest -> ExceptT Error H.Transaction Wai.Response) ->
|
||||||
ApiRequest -> ExceptT Error H.Transaction Response
|
ApiRequest -> ExceptT Error H.Transaction Wai.Response
|
||||||
runPgLocals conf claims app req = do
|
runPgLocals conf claims app req = do
|
||||||
lift $ H.statement mempty $ H.dynamicallyParameterized
|
lift $ H.statement mempty $ H.dynamicallyParameterized
|
||||||
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql))
|
("select " <> intercalateSnippet ", " (searchPathSql : roleSql ++ claimsSql ++ [methodSql, pathSql] ++ headersSql ++ cookiesSql ++ appSettingsSql))
|
||||||
@@ -78,11 +77,16 @@ runPgLocals conf claims app req = do
|
|||||||
setConfigLocal mempty ("search_path", schemas)
|
setConfigLocal mempty ("search_path", schemas)
|
||||||
preReqSql = (\f -> "select " <> toS f <> "();") <$> configDbPreRequest conf
|
preReqSql = (\f -> "select " <> toS f <> "();") <$> configDbPreRequest conf
|
||||||
|
|
||||||
|
-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.
|
||||||
|
setConfigLocal :: Text -> (Text, Text) -> H.Snippet
|
||||||
|
setConfigLocal prefix (k, v) =
|
||||||
|
"set_config(" <> unknownLiteral (prefix <> k) <> ", " <> unknownLiteral v <> ", true)"
|
||||||
|
|
||||||
-- | Log in apache format. Only requests that have a status greater than minStatus are logged.
|
-- | Log in apache format. Only requests that have a status greater than minStatus are logged.
|
||||||
-- | There's no way to filter logs in the apache format on wai-extra: https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html#t:OutputFormat.
|
-- | There's no way to filter logs in the apache format on wai-extra: https://hackage.haskell.org/package/wai-extra-3.0.29.2/docs/Network-Wai-Middleware-RequestLogger.html#t:OutputFormat.
|
||||||
-- | So here we copy wai-logger apacheLogStr function: https://github.com/kazu-yamamoto/logger/blob/a4f51b909a099c51af7a3f75cf16e19a06f9e257/wai-logger/Network/Wai/Logger/Apache.hs#L45
|
-- | So here we copy wai-logger apacheLogStr function: https://github.com/kazu-yamamoto/logger/blob/a4f51b909a099c51af7a3f75cf16e19a06f9e257/wai-logger/Network/Wai/Logger/Apache.hs#L45
|
||||||
-- | TODO: Add the ability to filter apache logs on wai-extra and remove this function.
|
-- | TODO: Add the ability to filter apache logs on wai-extra and remove this function.
|
||||||
pgrstFormat :: Status -> OutputFormatter
|
pgrstFormat :: Status -> Wai.OutputFormatter
|
||||||
pgrstFormat minStatus date req status responseSize =
|
pgrstFormat minStatus date req status responseSize =
|
||||||
if status < minStatus
|
if status < minStatus
|
||||||
then mempty
|
then mempty
|
||||||
@@ -90,55 +94,55 @@ pgrstFormat minStatus date req status responseSize =
|
|||||||
<> " - - ["
|
<> " - - ["
|
||||||
<> toLogStr date
|
<> toLogStr date
|
||||||
<> "] \""
|
<> "] \""
|
||||||
<> toLogStr (requestMethod req)
|
<> toLogStr (Wai.requestMethod req)
|
||||||
<> " "
|
<> " "
|
||||||
<> toLogStr (rawPathInfo req <> rawQueryString req)
|
<> toLogStr (Wai.rawPathInfo req <> Wai.rawQueryString req)
|
||||||
<> " "
|
<> " "
|
||||||
<> toLogStr (show (httpVersion req)::Text)
|
<> toLogStr (show (Wai.httpVersion req)::Text)
|
||||||
<> "\" "
|
<> "\" "
|
||||||
<> toLogStr (show (statusCode status)::Text)
|
<> toLogStr (show (statusCode status)::Text)
|
||||||
<> " "
|
<> " "
|
||||||
<> toLogStr (maybe "-" show responseSize::Text)
|
<> toLogStr (maybe "-" show responseSize::Text)
|
||||||
<> " \""
|
<> " \""
|
||||||
<> toLogStr (fromMaybe mempty $ requestHeaderReferer req)
|
<> toLogStr (fromMaybe mempty $ Wai.requestHeaderReferer req)
|
||||||
<> "\" \""
|
<> "\" \""
|
||||||
<> toLogStr (fromMaybe mempty $ requestHeaderUserAgent req)
|
<> toLogStr (fromMaybe mempty $ Wai.requestHeaderUserAgent req)
|
||||||
<> "\"\n"
|
<> "\"\n"
|
||||||
where
|
where
|
||||||
getSourceFromSocket = BS.pack . showSockAddr . remoteHost
|
getSourceFromSocket = BS.pack . Wai.showSockAddr . Wai.remoteHost
|
||||||
|
|
||||||
pgrstMiddleware :: LogLevel -> Application -> Application
|
pgrstMiddleware :: LogLevel -> Wai.Application -> Wai.Application
|
||||||
pgrstMiddleware logLevel =
|
pgrstMiddleware logLevel =
|
||||||
logger
|
logger
|
||||||
. gzip def
|
. Wai.gzip Wai.def
|
||||||
. cors corsPolicy
|
. Wai.cors corsPolicy
|
||||||
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
. Wai.staticPolicy (Wai.only [("favicon.ico", "static/favicon.ico")])
|
||||||
where
|
where
|
||||||
logger = case logLevel of
|
logger = case logLevel of
|
||||||
LogCrit -> id
|
LogCrit -> id
|
||||||
LogError -> unsafePerformIO $ mkRequestLogger def { outputFormat = CustomOutputFormat $ pgrstFormat status500}
|
LogError -> unsafePerformIO $ Wai.mkRequestLogger Wai.def { Wai.outputFormat = Wai.CustomOutputFormat $ pgrstFormat status500}
|
||||||
LogWarn -> unsafePerformIO $ mkRequestLogger def { outputFormat = CustomOutputFormat $ pgrstFormat status400}
|
LogWarn -> unsafePerformIO $ Wai.mkRequestLogger Wai.def { Wai.outputFormat = Wai.CustomOutputFormat $ pgrstFormat status400}
|
||||||
LogInfo -> logStdout
|
LogInfo -> Wai.logStdout
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
defaultCorsPolicy :: Wai.CorsResourcePolicy
|
||||||
defaultCorsPolicy = CorsResourcePolicy Nothing
|
defaultCorsPolicy = Wai.CorsResourcePolicy Nothing
|
||||||
["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
["GET", "POST", "PATCH", "PUT", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
||||||
(Just $ 60*60*24) False False True
|
(Just $ 60*60*24) False False True
|
||||||
|
|
||||||
-- | CORS policy to be used in by Wai Cors middleware
|
-- | CORS policy to be used in by Wai Cors middleware
|
||||||
corsPolicy :: Request -> Maybe CorsResourcePolicy
|
corsPolicy :: Wai.Request -> Maybe Wai.CorsResourcePolicy
|
||||||
corsPolicy req = case lookup "origin" headers of
|
corsPolicy req = case lookup "origin" headers of
|
||||||
Just origin -> Just defaultCorsPolicy {
|
Just origin -> Just defaultCorsPolicy {
|
||||||
corsOrigins = Just ([origin], True)
|
Wai.corsOrigins = Just ([origin], True)
|
||||||
, corsRequestHeaders = "Authentication":accHeaders
|
, Wai.corsRequestHeaders = "Authentication" : accHeaders
|
||||||
, corsExposedHeaders = Just [
|
, Wai.corsExposedHeaders = Just [
|
||||||
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
|
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
|
||||||
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
|
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
Nothing -> Nothing
|
Nothing -> Nothing
|
||||||
where
|
where
|
||||||
headers = requestHeaders req
|
headers = Wai.requestHeaders req
|
||||||
accHeaders = case lookup "access-control-request-headers" headers of
|
accHeaders = case lookup "access-control-request-headers" headers of
|
||||||
Just hdrs -> map (CI.mk . toS . T.strip . toS) $ BS.split ',' hdrs
|
Just hdrs -> map (CI.mk . toS . T.strip . toS) $ BS.split ',' hdrs
|
||||||
Nothing -> []
|
Nothing -> []
|
||||||
@@ -164,15 +168,15 @@ optionalRollback AppConfig{..} ApiRequest{..} transaction = do
|
|||||||
return $ Wai.mapResponseHeaders preferenceApplied resp
|
return $ Wai.mapResponseHeaders preferenceApplied resp
|
||||||
where
|
where
|
||||||
shouldCommit =
|
shouldCommit =
|
||||||
configDbTxAllowOverride && iPreferTransaction == Just Types.Commit
|
configDbTxAllowOverride && iPreferTransaction == Just Commit
|
||||||
shouldRollback =
|
shouldRollback =
|
||||||
configDbTxAllowOverride && iPreferTransaction == Just Types.Rollback
|
configDbTxAllowOverride && iPreferTransaction == Just Rollback
|
||||||
preferenceApplied
|
preferenceApplied
|
||||||
| shouldCommit =
|
| shouldCommit =
|
||||||
Types.addHeadersIfNotIncluded
|
addHeadersIfNotIncluded
|
||||||
[(HTTP.hPreferenceApplied, BS.pack (show Types.Commit))]
|
[(HTTP.hPreferenceApplied, BS.pack (show Commit))]
|
||||||
| shouldRollback =
|
| shouldRollback =
|
||||||
Types.addHeadersIfNotIncluded
|
addHeadersIfNotIncluded
|
||||||
[(HTTP.hPreferenceApplied, BS.pack (show Types.Rollback))]
|
[(HTTP.hPreferenceApplied, BS.pack (show Rollback))]
|
||||||
| otherwise =
|
| otherwise =
|
||||||
identity
|
identity
|
||||||
|
|||||||
+45
-43
@@ -9,31 +9,33 @@ import qualified Data.Aeson as JSON
|
|||||||
import qualified Data.ByteString.Lazy as LBS
|
import qualified Data.ByteString.Lazy as LBS
|
||||||
import qualified Data.HashMap.Strict as HashMap
|
import qualified Data.HashMap.Strict as HashMap
|
||||||
import qualified Data.HashSet.InsOrd as Set
|
import qualified Data.HashSet.InsOrd as Set
|
||||||
|
import qualified Data.Text as T
|
||||||
|
|
||||||
import Control.Arrow ((&&&))
|
import Control.Arrow ((&&&))
|
||||||
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Data.String (IsString (..))
|
import Data.String (IsString (..))
|
||||||
import Data.Text (append, breakOn, dropWhile, init,
|
|
||||||
intercalate, pack, tail, toLower,
|
|
||||||
unpack)
|
|
||||||
import Network.URI (URI (..), URIAuth (..))
|
import Network.URI (URI (..), URIAuth (..))
|
||||||
|
|
||||||
import Control.Lens
|
import Control.Lens (at, (.~), (?~))
|
||||||
|
|
||||||
import Data.Swagger
|
import Data.Swagger
|
||||||
|
|
||||||
import PostgREST.ApiRequest (ContentType (..))
|
import PostgREST.Config (AppConfig (..), Proxy (..),
|
||||||
import PostgREST.Config (AppConfig (..), docsVersion,
|
docsVersion,
|
||||||
prettyVersion)
|
isMalformedProxyUri,
|
||||||
import PostgREST.Private.ProxyUri (isMalformedProxyUri, toURI)
|
prettyVersion, toURI)
|
||||||
import PostgREST.Types (Column (..), DbStructure (..),
|
import PostgREST.DbStructure (DbStructure (..), tableCols,
|
||||||
ForeignKey (..), PgArg (..),
|
tablePKCols)
|
||||||
PrimaryKey (..),
|
import PostgREST.DbStructure.Proc (PgArg (..),
|
||||||
ProcDescription (..), Proxy (..),
|
ProcDescription (..))
|
||||||
Table (..), tableCols, tableName,
|
import PostgREST.DbStructure.Relation (PrimaryKey (..))
|
||||||
tablePKCols, tableSchema, toMime)
|
import PostgREST.DbStructure.Table (Column (..), ForeignKey (..),
|
||||||
import Protolude hiding (Proxy, dropWhile, get,
|
Table (..))
|
||||||
intercalate, toLower, toS, (&))
|
|
||||||
|
import PostgREST.ContentType
|
||||||
|
|
||||||
|
import Protolude hiding (Proxy, get, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
encode :: AppConfig -> DbStructure -> [Table] -> Maybe Text -> HashMap.HashMap k [ProcDescription] -> LBS.ByteString
|
encode :: AppConfig -> DbStructure -> [Table] -> Maybe Text -> HashMap.HashMap k [ProcDescription] -> LBS.ByteString
|
||||||
@@ -47,7 +49,7 @@ encode conf dbStructure tables schemaDescription procs =
|
|||||||
(dbPrimaryKeys dbStructure)
|
(dbPrimaryKeys dbStructure)
|
||||||
|
|
||||||
makeMimeList :: [ContentType] -> MimeList
|
makeMimeList :: [ContentType] -> MimeList
|
||||||
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
|
makeMimeList cs = MimeList $ fmap (fromString . toS . toMime) cs
|
||||||
|
|
||||||
toSwaggerType :: Text -> SwaggerType t
|
toSwaggerType :: Text -> SwaggerType t
|
||||||
toSwaggerType "character varying" = SwaggerString
|
toSwaggerType "character varying" = SwaggerString
|
||||||
@@ -68,15 +70,15 @@ makeTableDef pks (t, cs, _) =
|
|||||||
(tn, (mempty :: Schema)
|
(tn, (mempty :: Schema)
|
||||||
& description .~ tableDescription t
|
& description .~ tableDescription t
|
||||||
& type_ ?~ SwaggerObject
|
& type_ ?~ SwaggerObject
|
||||||
& properties .~ fromList (map (makeProperty pks) cs)
|
& properties .~ fromList (fmap (makeProperty pks) cs)
|
||||||
& required .~ map colName (filter (not . colNullable) cs))
|
& required .~ fmap colName (filter (not . colNullable) cs))
|
||||||
|
|
||||||
makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema)
|
makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema)
|
||||||
makeProperty pks c = (colName c, Inline s)
|
makeProperty pks c = (colName c, Inline s)
|
||||||
where
|
where
|
||||||
e = if null $ colEnum c then Nothing else JSON.decode $ JSON.encode $ colEnum c
|
e = if null $ colEnum c then Nothing else JSON.decode $ JSON.encode $ colEnum c
|
||||||
fk ForeignKey{fkCol=Column{colTable=Table{tableName=a}, colName=b}} =
|
fk ForeignKey{fkCol=Column{colTable=Table{tableName=a}, colName=b}} =
|
||||||
intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]
|
T.intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]
|
||||||
pk :: Bool
|
pk :: Bool
|
||||||
pk = any (\p -> pkTable p == colTable c && pkName p == colName c) pks
|
pk = any (\p -> pkTable p == colTable c && pkName p == colName c) pks
|
||||||
n = catMaybes
|
n = catMaybes
|
||||||
@@ -86,7 +88,7 @@ makeProperty pks c = (colName c, Inline s)
|
|||||||
]
|
]
|
||||||
d =
|
d =
|
||||||
if length n > 1 then
|
if length n > 1 then
|
||||||
Just $ append (maybe "" (`append` "\n\n") $ colDescription c) (intercalate "\n" n)
|
Just $ T.append (maybe "" (`T.append` "\n\n") $ colDescription c) (T.intercalate "\n" n)
|
||||||
else
|
else
|
||||||
colDescription c
|
colDescription c
|
||||||
s =
|
s =
|
||||||
@@ -103,8 +105,8 @@ makeProcSchema pd =
|
|||||||
(mempty :: Schema)
|
(mempty :: Schema)
|
||||||
& description .~ pdDescription pd
|
& description .~ pdDescription pd
|
||||||
& type_ ?~ SwaggerObject
|
& type_ ?~ SwaggerObject
|
||||||
& properties .~ fromList (map makeProcProperty (pdArgs pd))
|
& properties .~ fromList (fmap makeProcProperty (pdArgs pd))
|
||||||
& required .~ map pgaName (filter pgaReq (pdArgs pd))
|
& required .~ fmap pgaName (filter pgaReq (pdArgs pd))
|
||||||
|
|
||||||
makeProcProperty :: PgArg -> (Text, Referenced Schema)
|
makeProcProperty :: PgArg -> (Text, Referenced Schema)
|
||||||
makeProcProperty (PgArg n t _ _) = (n, Inline s)
|
makeProcProperty (PgArg n t _ _) = (n, Inline s)
|
||||||
@@ -203,7 +205,7 @@ makeObjectBody tn =
|
|||||||
|
|
||||||
makeRowFilter :: Text -> Column -> (Text, Param)
|
makeRowFilter :: Text -> Column -> (Text, Param)
|
||||||
makeRowFilter tn c =
|
makeRowFilter tn c =
|
||||||
(intercalate "." ["rowFilter", tn, colName c], (mempty :: Param)
|
(T.intercalate "." ["rowFilter", tn, colName c], (mempty :: Param)
|
||||||
& name .~ colName c
|
& name .~ colName c
|
||||||
& description .~ colDescription c
|
& description .~ colDescription c
|
||||||
& required ?~ False
|
& required ?~ False
|
||||||
@@ -213,21 +215,21 @@ makeRowFilter tn c =
|
|||||||
& format ?~ colType c))
|
& format ?~ colType c))
|
||||||
|
|
||||||
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
|
||||||
makeRowFilters tn = map (makeRowFilter tn)
|
makeRowFilters tn = fmap (makeRowFilter tn)
|
||||||
|
|
||||||
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
|
||||||
makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
makePathItem (t, cs, _) = ("/" ++ T.unpack tn, p $ tableInsertable t)
|
||||||
where
|
where
|
||||||
-- Use first line of table description as summary; rest as description (if present)
|
-- Use first line of table description as summary; rest as description (if present)
|
||||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||||
(tSum, tDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
|
(tSum, tDesc) = fmap fst &&& fmap (T.dropWhile (=='\n') . snd) $
|
||||||
breakOn "\n" <$> tableDescription t
|
T.breakOn "\n" <$> tableDescription t
|
||||||
tOp = (mempty :: Operation)
|
tOp = (mempty :: Operation)
|
||||||
& tags .~ Set.fromList [tn]
|
& tags .~ Set.fromList [tn]
|
||||||
& summary .~ tSum
|
& summary .~ tSum
|
||||||
& description .~ mfilter (/="") tDesc
|
& description .~ mfilter (/="") tDesc
|
||||||
getOp = tOp
|
getOp = tOp
|
||||||
& parameters .~ map ref (rs <> ["select", "order", "range", "rangeUnit", "offset", "limit", "preferCount"])
|
& parameters .~ fmap ref (rs <> ["select", "order", "range", "rangeUnit", "offset", "limit", "preferCount"])
|
||||||
& at 206 ?~ "Partial Content"
|
& at 206 ?~ "Partial Content"
|
||||||
& at 200 ?~ Inline ((mempty :: Response)
|
& at 200 ?~ Inline ((mempty :: Response)
|
||||||
& description .~ "OK"
|
& description .~ "OK"
|
||||||
@@ -237,20 +239,20 @@ makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
|
|||||||
)
|
)
|
||||||
)
|
)
|
||||||
postOp = tOp
|
postOp = tOp
|
||||||
& parameters .~ map ref ["body." <> tn, "select", "preferReturn"]
|
& parameters .~ fmap ref ["body." <> tn, "select", "preferReturn"]
|
||||||
& at 201 ?~ "Created"
|
& at 201 ?~ "Created"
|
||||||
patchOp = tOp
|
patchOp = tOp
|
||||||
& parameters .~ map ref (rs <> ["body." <> tn, "preferReturn"])
|
& parameters .~ fmap ref (rs <> ["body." <> tn, "preferReturn"])
|
||||||
& at 204 ?~ "No Content"
|
& at 204 ?~ "No Content"
|
||||||
deletOp = tOp
|
deletOp = tOp
|
||||||
& parameters .~ map ref (rs <> ["preferReturn"])
|
& parameters .~ fmap ref (rs <> ["preferReturn"])
|
||||||
& at 204 ?~ "No Content"
|
& at 204 ?~ "No Content"
|
||||||
pr = (mempty :: PathItem) & get ?~ getOp
|
pr = (mempty :: PathItem) & get ?~ getOp
|
||||||
pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp
|
pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp
|
||||||
p False = pr
|
p False = pr
|
||||||
p True = pw
|
p True = pw
|
||||||
tn = tableName t
|
tn = tableName t
|
||||||
rs = [ intercalate "." ["rowFilter", tn, colName c ] | c <- cs ]
|
rs = [ T.intercalate "." ["rowFilter", tn, colName c ] | c <- cs ]
|
||||||
ref = Ref . Reference
|
ref = Ref . Reference
|
||||||
|
|
||||||
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
|
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
|
||||||
@@ -258,8 +260,8 @@ makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
|
|||||||
where
|
where
|
||||||
-- Use first line of proc description as summary; rest as description (if present)
|
-- Use first line of proc description as summary; rest as description (if present)
|
||||||
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
-- We strip leading newlines from description so that users can include a blank line between summary and description
|
||||||
(pSum, pDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
|
(pSum, pDesc) = fmap fst &&& fmap (T.dropWhile (=='\n') . snd) $
|
||||||
breakOn "\n" <$> pdDescription pd
|
T.breakOn "\n" <$> pdDescription pd
|
||||||
postOp = (mempty :: Operation)
|
postOp = (mempty :: Operation)
|
||||||
& summary .~ pSum
|
& summary .~ pSum
|
||||||
& description .~ mfilter (/="") pDesc
|
& description .~ mfilter (/="") pDesc
|
||||||
@@ -282,7 +284,7 @@ makeRootPathItem = ("/", p)
|
|||||||
|
|
||||||
makePathItems :: [ProcDescription] -> [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
|
makePathItems :: [ProcDescription] -> [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
|
||||||
makePathItems pds ti = fromList $ makeRootPathItem :
|
makePathItems pds ti = fromList $ makeRootPathItem :
|
||||||
map makePathItem ti ++ map makeProcPathItem pds
|
fmap makePathItem ti ++ fmap makeProcPathItem pds
|
||||||
|
|
||||||
escapeHostName :: Text -> Text
|
escapeHostName :: Text -> Text
|
||||||
escapeHostName "*" = "0.0.0.0"
|
escapeHostName "*" = "0.0.0.0"
|
||||||
@@ -294,7 +296,7 @@ escapeHostName h = h
|
|||||||
|
|
||||||
postgrestSpec :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> Swagger
|
postgrestSpec :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> Swagger
|
||||||
postgrestSpec pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
|
postgrestSpec pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
|
||||||
& basePath ?~ unpack b
|
& basePath ?~ T.unpack b
|
||||||
& schemes ?~ [s']
|
& schemes ?~ [s']
|
||||||
& info .~ ((mempty :: Info)
|
& info .~ ((mempty :: Info)
|
||||||
& version .~ prettyVersion
|
& version .~ prettyVersion
|
||||||
@@ -304,14 +306,14 @@ postgrestSpec pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
|
|||||||
& description ?~ "PostgREST Documentation"
|
& description ?~ "PostgREST Documentation"
|
||||||
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
|
||||||
& host .~ h'
|
& host .~ h'
|
||||||
& definitions .~ fromList (map (makeTableDef pks) ti)
|
& definitions .~ fromList (makeTableDef pks <$> ti)
|
||||||
& parameters .~ fromList (makeParamDefs ti)
|
& parameters .~ fromList (makeParamDefs ti)
|
||||||
& paths .~ makePathItems pds ti
|
& paths .~ makePathItems pds ti
|
||||||
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
& consumes .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
& consumes .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
|
||||||
where
|
where
|
||||||
s' = if s == "http" then Http else Https
|
s' = if s == "http" then Http else Https
|
||||||
h' = Just $ Host (unpack $ escapeHostName h) (Just (fromInteger p))
|
h' = Just $ Host (T.unpack $ escapeHostName h) (Just (fromInteger p))
|
||||||
d = fromMaybe "This is a dynamic API generated by PostgREST" sd
|
d = fromMaybe "This is a dynamic API generated by PostgREST" sd
|
||||||
|
|
||||||
pickProxy :: Maybe Text -> Maybe Proxy
|
pickProxy :: Maybe Text -> Maybe Proxy
|
||||||
@@ -329,19 +331,19 @@ pickProxy proxy
|
|||||||
}
|
}
|
||||||
where
|
where
|
||||||
uri = toURI $ fromJust proxy
|
uri = toURI $ fromJust proxy
|
||||||
scheme = init $ toLower $ pack $ uriScheme uri
|
scheme = T.init $ T.toLower $ T.pack $ uriScheme uri
|
||||||
path URI {uriPath = ""} = "/"
|
path URI {uriPath = ""} = "/"
|
||||||
path URI {uriPath = p} = p
|
path URI {uriPath = p} = p
|
||||||
path' = pack $ path uri
|
path' = T.pack $ path uri
|
||||||
authority = fromJust $ uriAuthority uri
|
authority = fromJust $ uriAuthority uri
|
||||||
host' = pack $ uriRegName authority
|
host' = T.pack $ uriRegName authority
|
||||||
port' = uriPort authority
|
port' = uriPort authority
|
||||||
readPort = fromMaybe 80 . readMaybe
|
readPort = fromMaybe 80 . readMaybe
|
||||||
port'' :: Integer
|
port'' :: Integer
|
||||||
port'' = case (port', scheme) of
|
port'' = case (port', scheme) of
|
||||||
("", "http") -> 80
|
("", "http") -> 80
|
||||||
("", "https") -> 443
|
("", "https") -> 443
|
||||||
_ -> readPort $ unpack $ tail $ pack port'
|
_ -> readPort $ T.unpack $ T.tail $ T.pack port'
|
||||||
|
|
||||||
proxyUri :: AppConfig -> (Text, Text, Integer, Text)
|
proxyUri :: AppConfig -> (Text, Text, Integer, Text)
|
||||||
proxyUri AppConfig{..} =
|
proxyUri AppConfig{..} =
|
||||||
|
|||||||
@@ -1,22 +1,18 @@
|
|||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE FlexibleContexts #-}
|
|
||||||
{-# LANGUAGE FlexibleInstances #-}
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.QueryBuilder
|
Module : PostgREST.Query.QueryBuilder
|
||||||
Description : PostgREST SQL queries generating functions.
|
Description : PostgREST SQL queries generating functions.
|
||||||
|
|
||||||
This module provides functions to consume data types that
|
This module provides functions to consume data types that
|
||||||
represent database queries (e.g. ReadRequest, MutateRequest) and SqlFragment
|
represent database queries (e.g. ReadRequest, MutateRequest) and SqlFragment
|
||||||
to produce SqlQuery type outputs.
|
to produce SqlQuery type outputs.
|
||||||
-}
|
-}
|
||||||
module PostgREST.QueryBuilder (
|
module PostgREST.Query.QueryBuilder
|
||||||
readRequestToQuery
|
( readRequestToQuery
|
||||||
, mutateRequestToQuery
|
, mutateRequestToQuery
|
||||||
, readRequestToCountQuery
|
, readRequestToCountQuery
|
||||||
, requestToCallProcQuery
|
, requestToCallProcQuery
|
||||||
, limitedQuery
|
, limitedQuery
|
||||||
, setConfigLocal
|
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
@@ -25,12 +21,20 @@ import qualified Hasql.DynamicStatements.Snippet as H
|
|||||||
|
|
||||||
import Data.Tree (Tree (..))
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import Data.Maybe
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
import PostgREST.Private.Common
|
QualifiedIdentifier (..))
|
||||||
import PostgREST.Private.QueryFragment
|
import PostgREST.DbStructure.Proc (PgArg (..))
|
||||||
import PostgREST.Types
|
import PostgREST.DbStructure.Relation (Cardinality (..),
|
||||||
import Protolude hiding (cast, intercalate,
|
Relation (..))
|
||||||
replace)
|
import PostgREST.DbStructure.Table (Table (..))
|
||||||
|
import PostgREST.Request.ApiRequest (PayloadJSON (..))
|
||||||
|
import PostgREST.Request.Preferences (PreferParameters (..),
|
||||||
|
PreferResolution (..))
|
||||||
|
|
||||||
|
import PostgREST.Query.SqlFragment
|
||||||
|
import PostgREST.Request.Types
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
readRequestToQuery :: ReadRequest -> H.Snippet
|
readRequestToQuery :: ReadRequest -> H.Snippet
|
||||||
readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
|
readRequestToQuery (Node (Select colSelects mainQi tblAlias implJoins logicForest joinConditions_ ordts range, _) forest) =
|
||||||
@@ -175,8 +179,3 @@ readRequestToCountQuery (Node (Select{from=qi, where_=logicForest}, _) _) =
|
|||||||
|
|
||||||
limitedQuery :: H.Snippet -> Maybe Integer -> H.Snippet
|
limitedQuery :: H.Snippet -> Maybe Integer -> H.Snippet
|
||||||
limitedQuery query maxRows = query <> H.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
limitedQuery query maxRows = query <> H.sql (maybe mempty (\x -> " LIMIT " <> BS.pack (show x)) maxRows)
|
||||||
|
|
||||||
-- | Do a pg set_config(setting, value, true) call. This is equivalent to a SET LOCAL.
|
|
||||||
setConfigLocal :: Text -> (Text, Text) -> H.Snippet
|
|
||||||
setConfigLocal prefix (k, v) =
|
|
||||||
"set_config(" <> unknownLiteral (prefix <> k) <> ", " <> unknownLiteral v <> ", true)"
|
|
||||||
@@ -1,42 +1,106 @@
|
|||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Private.QueryFragment
|
Module : PostgREST.Query.SqlFragment
|
||||||
Description : Helper functions for PostgREST.QueryBuilder.
|
Description : Helper functions for PostgREST.QueryBuilder.
|
||||||
|
|
||||||
Any function that outputs a SqlFragment should be in this module.
|
Any function that outputs a SqlFragment should be in this module.
|
||||||
-}
|
-}
|
||||||
module PostgREST.Private.QueryFragment where
|
module PostgREST.Query.SqlFragment
|
||||||
|
( noLocationF
|
||||||
|
, SqlFragment
|
||||||
|
, asBinaryF
|
||||||
|
, asCsvF
|
||||||
|
, asJsonF
|
||||||
|
, asJsonSingleF
|
||||||
|
, countF
|
||||||
|
, fromQi
|
||||||
|
, ftsOperators
|
||||||
|
, jsonPlaceHolder
|
||||||
|
, limitOffsetF
|
||||||
|
, locationF
|
||||||
|
, normalizedBody
|
||||||
|
, operators
|
||||||
|
, pgFmtColumn
|
||||||
|
, pgFmtIdent
|
||||||
|
, pgFmtJoinCondition
|
||||||
|
, pgFmtLogicTree
|
||||||
|
, pgFmtOrderTerm
|
||||||
|
, pgFmtSelectItem
|
||||||
|
, responseHeadersF
|
||||||
|
, responseStatusF
|
||||||
|
, returningF
|
||||||
|
, selectBody
|
||||||
|
, sourceCTEName
|
||||||
|
, unknownLiteral
|
||||||
|
, intercalateSnippet
|
||||||
|
) where
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS (intercalate,
|
import qualified Data.ByteString.Char8 as BS
|
||||||
pack, unwords)
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
import qualified Data.ByteString.Lazy as BL
|
||||||
import qualified Data.HashMap.Strict as HM
|
import qualified Data.HashMap.Strict as HM
|
||||||
import Data.Maybe
|
import qualified Data.Text as T
|
||||||
import qualified Data.Text as T (intercalate,
|
|
||||||
isInfixOf, map,
|
|
||||||
null, replace,
|
|
||||||
takeWhile,
|
|
||||||
toLower)
|
|
||||||
import qualified Hasql.DynamicStatements.Snippet as H
|
import qualified Hasql.DynamicStatements.Snippet as H
|
||||||
import PostgREST.RangeQuery (NonnegRange,
|
import qualified Hasql.Encoders as HE
|
||||||
allRange,
|
|
||||||
rangeLimit,
|
import Data.Foldable (foldr1)
|
||||||
rangeOffset)
|
|
||||||
import PostgREST.Types
|
|
||||||
import Protolude hiding (cast,
|
|
||||||
intercalate,
|
|
||||||
replace, toLower,
|
|
||||||
toS)
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
import Text.InterpolatedString.Perl6 (qc)
|
import Text.InterpolatedString.Perl6 (qc)
|
||||||
|
|
||||||
import qualified Hasql.Encoders as HE
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
import PostgREST.Private.Common
|
QualifiedIdentifier (..))
|
||||||
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion96)
|
||||||
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
|
rangeLimit, rangeOffset)
|
||||||
|
import PostgREST.Request.Types (Alias, Field, Filter (..),
|
||||||
|
JoinCondition (..),
|
||||||
|
JsonOperand (..),
|
||||||
|
JsonOperation (..),
|
||||||
|
JsonPath, LogicTree (..),
|
||||||
|
OpExpr (..), Operation (..),
|
||||||
|
OrderTerm (..), SelectItem)
|
||||||
|
|
||||||
|
import Protolude hiding (cast, toS)
|
||||||
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
|
||||||
|
-- | A part of a SQL query that cannot be executed independently
|
||||||
|
type SqlFragment = ByteString
|
||||||
|
|
||||||
noLocationF :: SqlFragment
|
noLocationF :: SqlFragment
|
||||||
noLocationF = "array[]::text[]"
|
noLocationF = "array[]::text[]"
|
||||||
|
|
||||||
|
sourceCTEName :: SqlFragment
|
||||||
|
sourceCTEName = "pgrst_source"
|
||||||
|
|
||||||
|
operators :: HM.HashMap Text SqlFragment
|
||||||
|
operators = HM.union (HM.fromList [
|
||||||
|
("eq", "="),
|
||||||
|
("gte", ">="),
|
||||||
|
("gt", ">"),
|
||||||
|
("lte", "<="),
|
||||||
|
("lt", "<"),
|
||||||
|
("neq", "<>"),
|
||||||
|
("like", "LIKE"),
|
||||||
|
("ilike", "ILIKE"),
|
||||||
|
("in", "IN"),
|
||||||
|
("is", "IS"),
|
||||||
|
("cs", "@>"),
|
||||||
|
("cd", "<@"),
|
||||||
|
("ov", "&&"),
|
||||||
|
("sl", "<<"),
|
||||||
|
("sr", ">>"),
|
||||||
|
("nxr", "&<"),
|
||||||
|
("nxl", "&>"),
|
||||||
|
("adj", "-|-")]) ftsOperators
|
||||||
|
|
||||||
|
ftsOperators :: HM.HashMap Text SqlFragment
|
||||||
|
ftsOperators = HM.fromList [
|
||||||
|
("fts", "@@ to_tsquery"),
|
||||||
|
("plfts", "@@ plainto_tsquery"),
|
||||||
|
("phfts", "@@ phraseto_tsquery"),
|
||||||
|
("wfts", "@@ websearch_to_tsquery")
|
||||||
|
]
|
||||||
|
|
||||||
-- |
|
-- |
|
||||||
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
|
-- These CTEs convert a json object into a json array, this way we can use json_populate_recordset for all json payloads
|
||||||
-- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays
|
-- Otherwise we'd have to use json_populate_record for json objects and json_populate_recordset for json arrays
|
||||||
@@ -245,9 +309,13 @@ currentSettingF setting =
|
|||||||
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
-- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
|
||||||
"nullif(current_setting(" <> pgFmtLit setting <> ", true), '')"
|
"nullif(current_setting(" <> pgFmtLit setting <> ", true), '')"
|
||||||
|
|
||||||
-- Hasql Snippet utilitarians
|
-- Hasql Snippet utilities
|
||||||
unknownEncoder :: ByteString -> H.Snippet
|
unknownEncoder :: ByteString -> H.Snippet
|
||||||
unknownEncoder = H.encoderAndParam (HE.nonNullable HE.unknown)
|
unknownEncoder = H.encoderAndParam (HE.nonNullable HE.unknown)
|
||||||
|
|
||||||
unknownLiteral :: Text -> H.Snippet
|
unknownLiteral :: Text -> H.Snippet
|
||||||
unknownLiteral = unknownEncoder . encodeUtf8
|
unknownLiteral = unknownEncoder . encodeUtf8
|
||||||
|
|
||||||
|
intercalateSnippet :: ByteString -> [H.Snippet] -> H.Snippet
|
||||||
|
intercalateSnippet _ [] = mempty
|
||||||
|
intercalateSnippet frag snippets = foldr1 (\a b -> a <> H.sql frag <> b) snippets
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Statements
|
Module : PostgREST.Query.Statements
|
||||||
Description : PostgREST single SQL statements.
|
Description : PostgREST single SQL statements.
|
||||||
|
|
||||||
This module constructs single SQL statements that can be parametrized and prepared.
|
This module constructs single SQL statements that can be parametrized and prepared.
|
||||||
@@ -10,38 +10,41 @@ This module constructs single SQL statements that can be parametrized and prepar
|
|||||||
|
|
||||||
TODO: Currently, createReadStatement is not using prepared statements. See https://github.com/PostgREST/postgrest/issues/718.
|
TODO: Currently, createReadStatement is not using prepared statements. See https://github.com/PostgREST/postgrest/issues/718.
|
||||||
-}
|
-}
|
||||||
module PostgREST.Statements (
|
module PostgREST.Query.Statements
|
||||||
createWriteStatement
|
( createWriteStatement
|
||||||
, createReadStatement
|
, createReadStatement
|
||||||
, callProcStatement
|
, callProcStatement
|
||||||
, createExplainStatement
|
, createExplainStatement
|
||||||
, dbSettingsStatement
|
, dbSettingsStatement
|
||||||
) where
|
) where
|
||||||
|
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
import Control.Lens ((^?))
|
|
||||||
import Data.Aeson as JSON
|
|
||||||
import qualified Data.Aeson.Lens as L
|
import qualified Data.Aeson.Lens as L
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import Data.Maybe
|
|
||||||
import Data.Text.Read (decimal)
|
|
||||||
import qualified Hasql.Decoders as HD
|
import qualified Hasql.Decoders as HD
|
||||||
import qualified Hasql.Encoders as HE
|
|
||||||
import qualified Hasql.Statement as H
|
|
||||||
import Network.HTTP.Types.Status
|
|
||||||
import PostgREST.Error
|
|
||||||
import PostgREST.Private.Common
|
|
||||||
import PostgREST.Private.QueryFragment
|
|
||||||
import PostgREST.Types
|
|
||||||
import Protolude hiding (cast,
|
|
||||||
replace, toS)
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
import qualified Hasql.DynamicStatements.Snippet as H
|
import qualified Hasql.DynamicStatements.Snippet as H
|
||||||
import qualified Hasql.DynamicStatements.Statement as H
|
import qualified Hasql.DynamicStatements.Statement as H
|
||||||
|
import qualified Hasql.Encoders as HE
|
||||||
|
import qualified Hasql.Statement as H
|
||||||
|
|
||||||
|
import Control.Lens ((^?))
|
||||||
|
import Data.Maybe (fromJust)
|
||||||
|
import Data.Text.Read (decimal)
|
||||||
|
import Network.HTTP.Types.Status (Status)
|
||||||
import Text.InterpolatedString.Perl6 (q)
|
import Text.InterpolatedString.Perl6 (q)
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.PgVersion (PgVersion)
|
||||||
|
import PostgREST.Error (Error (..))
|
||||||
|
import PostgREST.GucHeader (GucHeader)
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Identifiers (FieldName)
|
||||||
|
import PostgREST.Query.SqlFragment
|
||||||
|
import PostgREST.Request.Preferences
|
||||||
|
|
||||||
|
import Protolude hiding (toS)
|
||||||
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
|
||||||
{-| The generic query result format used by API responses. The location header
|
{-| The generic query result format used by API responses. The location header
|
||||||
is represented as a list of strings containing variable bindings like
|
is represented as a list of strings containing variable bindings like
|
||||||
@"k1=eq.42"@, or the empty list if there is no location header.
|
@"k1=eq.42"@, or the empty list if there is no location header.
|
||||||
@@ -216,3 +219,12 @@ dbSettingsStatement = H.Statement sql HE.noParams decodeSettings False
|
|||||||
order by key, setdatabase desc;
|
order by key, setdatabase desc;
|
||||||
|]
|
|]
|
||||||
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
decodeSettings = HD.rowList $ (,) <$> column HD.text <*> column HD.text
|
||||||
|
|
||||||
|
column :: HD.Value a -> HD.Row a
|
||||||
|
column = HD.column . HD.nonNullable
|
||||||
|
|
||||||
|
nullableColumn :: HD.Value a -> HD.Row (Maybe a)
|
||||||
|
nullableColumn = HD.column . HD.nullable
|
||||||
|
|
||||||
|
arrayColumn :: HD.Value a -> HD.Row [a]
|
||||||
|
arrayColumn = column . HD.listArray . HD.nonNullable
|
||||||
@@ -1,17 +1,18 @@
|
|||||||
{-|
|
{-|
|
||||||
Module : PostgREST.ApiRequest
|
Module : PostgREST.Request.ApiRequest
|
||||||
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE MultiWayIf #-}
|
{-# LANGUAGE MultiWayIf #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
|
|
||||||
module PostgREST.ApiRequest (
|
module PostgREST.Request.ApiRequest
|
||||||
ApiRequest(..)
|
( ApiRequest(..)
|
||||||
, InvokeMethod(..)
|
, InvokeMethod(..)
|
||||||
, ContentType(..)
|
, ContentType(..)
|
||||||
, Action(..)
|
, Action(..)
|
||||||
, Target(..)
|
, Target(..)
|
||||||
|
, PayloadJSON(..)
|
||||||
, mutuallyAgreeable
|
, mutuallyAgreeable
|
||||||
, userApiRequest
|
, userApiRequest
|
||||||
) where
|
) where
|
||||||
@@ -32,6 +33,7 @@ import Data.Aeson.Types (emptyArray, emptyObject)
|
|||||||
import Data.List (last, lookup, partition)
|
import Data.List (last, lookup, partition)
|
||||||
import Data.List.NonEmpty (head)
|
import Data.List.NonEmpty (head)
|
||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
|
import Data.Ranged.Boundaries (Boundary (..))
|
||||||
import Data.Ranged.Ranges (Range (..), emptyRange,
|
import Data.Ranged.Ranges (Range (..), emptyRange,
|
||||||
rangeIntersection)
|
rangeIntersection)
|
||||||
import Network.HTTP.Base (urlEncodeVars)
|
import Network.HTTP.Base (urlEncodeVars)
|
||||||
@@ -42,20 +44,48 @@ import Network.Wai (Request (..))
|
|||||||
import Network.Wai.Parse (parseHttpAccept)
|
import Network.Wai.Parse (parseHttpAccept)
|
||||||
import Web.Cookie (parseCookiesText)
|
import Web.Cookie (parseCookiesText)
|
||||||
|
|
||||||
|
import PostgREST.ContentType (ContentType (..))
|
||||||
import Data.Ranged.Boundaries
|
import PostgREST.DbStructure (DbStructure (..))
|
||||||
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier (..),
|
||||||
|
Schema)
|
||||||
|
import PostgREST.DbStructure.Proc (PgArg (..),
|
||||||
|
ProcDescription (..),
|
||||||
|
findProc)
|
||||||
import PostgREST.Error (ApiRequestError (..))
|
import PostgREST.Error (ApiRequestError (..))
|
||||||
import PostgREST.Parsers (pRequestColumns)
|
import PostgREST.Query.SqlFragment (ftsOperators, operators)
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange, rangeGeq,
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
rangeLimit, rangeOffset, rangeRequested,
|
rangeGeq, rangeLimit,
|
||||||
|
rangeOffset, rangeRequested,
|
||||||
restrictRange)
|
restrictRange)
|
||||||
import PostgREST.Types
|
import PostgREST.Request.Parsers (pRequestColumns)
|
||||||
|
import PostgREST.Request.Preferences (PreferCount (..),
|
||||||
|
PreferParameters (..),
|
||||||
|
PreferRepresentation (..),
|
||||||
|
PreferResolution (..),
|
||||||
|
PreferTransaction (..))
|
||||||
|
|
||||||
|
import qualified PostgREST.ContentType as ContentType
|
||||||
|
|
||||||
import Protolude hiding (head, toS)
|
import Protolude hiding (head, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
|
||||||
type RequestBody = BL.ByteString
|
type RequestBody = BL.ByteString
|
||||||
|
|
||||||
|
data PayloadJSON
|
||||||
|
= ProcessedJSON -- ^ Cached attributes of a JSON payload
|
||||||
|
{ pjRaw :: BL.ByteString
|
||||||
|
-- ^ This is the raw ByteString that comes from the request body. We
|
||||||
|
-- cache this instead of an Aeson Value because it was detected that for
|
||||||
|
-- large payloads the encoding had high memory usage, see
|
||||||
|
-- https://github.com/PostgREST/postgrest/pull/1005 for more details
|
||||||
|
, pjKeys :: S.Set Text
|
||||||
|
-- ^ Keys of the object or if it's an array these keys are guaranteed to
|
||||||
|
-- be the same across all its objects
|
||||||
|
}
|
||||||
|
| RawJSON { pjRaw :: BL.ByteString }
|
||||||
|
|
||||||
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
data InvokeMethod = InvHead | InvGet | InvPost deriving Eq
|
||||||
-- | Types of things a user wants to do to tables/views/procs
|
-- | Types of things a user wants to do to tables/views/procs
|
||||||
data Action = ActionCreate | ActionRead{isHead :: Bool}
|
data Action = ActionCreate | ActionRead{isHead :: Bool}
|
||||||
@@ -143,7 +173,7 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
|
|||||||
, iTarget = target
|
, iTarget = target
|
||||||
, iRange = ranges
|
, iRange = ranges
|
||||||
, iTopLevelRange = topLevelRange
|
, iTopLevelRange = topLevelRange
|
||||||
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
, iAccepts = maybe [CTAny] (map ContentType.decodeContentType . parseHttpAccept) $ lookupHeader "accept"
|
||||||
, iPayload = relevantPayload
|
, iPayload = relevantPayload
|
||||||
, iPreferRepresentation = representation
|
, iPreferRepresentation = representation
|
||||||
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
|
, iPreferParameters = if | hasPrefer (show SingleObject) -> Just SingleObject
|
||||||
@@ -202,7 +232,7 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
|
|||||||
isTargetingDefaultSpec = case target of
|
isTargetingDefaultSpec = case target of
|
||||||
TargetDefaultSpec _ -> True
|
TargetDefaultSpec _ -> True
|
||||||
_ -> False
|
_ -> False
|
||||||
contentType = decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
contentType = ContentType.decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type"
|
||||||
columns
|
columns
|
||||||
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
| action `elem` [ActionCreate, ActionUpdate, ActionInvoke InvPost] = toS <$> join (lookup "columns" qParams)
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
@@ -236,7 +266,7 @@ userApiRequest confSchemas rootSpec dbStructure req reqBody
|
|||||||
let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> urlEncodedBody in
|
let paramsMap = M.fromList $ (toS *** JSON.String . toS) <$> urlEncodedBody in
|
||||||
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
|
Right $ ProcessedJSON (JSON.encode paramsMap) $ S.fromList (M.keys paramsMap)
|
||||||
ct ->
|
ct ->
|
||||||
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
|
Left $ toS $ "Content-Type not acceptable: " <> ContentType.toMime ct
|
||||||
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows
|
||||||
action =
|
action =
|
||||||
case method of
|
case method of
|
||||||
@@ -1,18 +1,22 @@
|
|||||||
{-|
|
{-|
|
||||||
Module : PostgREST.DbRequestBuilder
|
Module : PostgREST.Request.DbRequestBuilder
|
||||||
Description : PostgREST database request builder
|
Description : PostgREST database request builder
|
||||||
|
|
||||||
This module is in charge of building an intermediate representation(ReadRequest, MutateRequest) between the HTTP request and the final resulting SQL query.
|
This module is in charge of building an intermediate
|
||||||
|
representation(ReadRequest, MutateRequest) between the HTTP request and the
|
||||||
|
final resulting SQL query.
|
||||||
|
|
||||||
A query tree is built in case of resource embedding. By inferring the relationship between tables, join conditions are added for every embedded resource.
|
A query tree is built in case of resource embedding. By inferring the
|
||||||
|
relationship between tables, join conditions are added for every embedded
|
||||||
|
resource.
|
||||||
-}
|
-}
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
{-# LANGUAGE LambdaCase #-}
|
{-# LANGUAGE LambdaCase #-}
|
||||||
{-# LANGUAGE NamedFieldPuns #-}
|
{-# LANGUAGE NamedFieldPuns #-}
|
||||||
{-# LANGUAGE RecordWildCards #-}
|
{-# LANGUAGE RecordWildCards #-}
|
||||||
|
|
||||||
module PostgREST.DbRequestBuilder (
|
module PostgREST.Request.DbRequestBuilder
|
||||||
readRequest
|
( readRequest
|
||||||
, mutateRequest
|
, mutateRequest
|
||||||
, returningCols
|
, returningCols
|
||||||
) where
|
) where
|
||||||
@@ -24,15 +28,30 @@ import Control.Arrow ((***))
|
|||||||
import Data.Either.Combinators (mapLeft)
|
import Data.Either.Combinators (mapLeft)
|
||||||
import Data.List (delete)
|
import Data.List (delete)
|
||||||
import Data.Text (isInfixOf)
|
import Data.Text (isInfixOf)
|
||||||
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
import Control.Applicative
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
import Data.Tree
|
QualifiedIdentifier (..),
|
||||||
|
Schema, TableName)
|
||||||
|
import PostgREST.DbStructure.Relation (Cardinality (..), Link (..),
|
||||||
|
Relation (..))
|
||||||
|
import PostgREST.DbStructure.Table (Column (..), Table (..),
|
||||||
|
tableQi)
|
||||||
|
import PostgREST.Error (ApiRequestError (..),
|
||||||
|
Error (..))
|
||||||
|
import PostgREST.Query.SqlFragment (sourceCTEName)
|
||||||
|
import PostgREST.RangeQuery (NonnegRange, allRange,
|
||||||
|
restrictRange)
|
||||||
|
import PostgREST.Request.ApiRequest (Action (..),
|
||||||
|
ApiRequest (..),
|
||||||
|
PayloadJSON (..))
|
||||||
|
|
||||||
|
import PostgREST.Request.Parsers
|
||||||
|
import PostgREST.Request.Preferences
|
||||||
|
import PostgREST.Request.Types
|
||||||
|
|
||||||
|
import qualified PostgREST.DbStructure.Relation as Relation
|
||||||
|
|
||||||
import PostgREST.ApiRequest (Action (..), ApiRequest (..))
|
|
||||||
import PostgREST.Error (ApiRequestError (..), Error (..))
|
|
||||||
import PostgREST.Parsers
|
|
||||||
import PostgREST.RangeQuery (NonnegRange, allRange, restrictRange)
|
|
||||||
import PostgREST.Types
|
|
||||||
import Protolude hiding (from)
|
import Protolude hiding (from)
|
||||||
|
|
||||||
-- | Builds the ReadRequest tree on a number of stages.
|
-- | Builds the ReadRequest tree on a number of stages.
|
||||||
@@ -50,23 +69,27 @@ readRequest schema rootTableName maxRows allRels apiRequest =
|
|||||||
(rootName, rootRels) = rootWithRels schema rootTableName allRels (iAction apiRequest)
|
(rootName, rootRels) = rootWithRels schema rootTableName allRels (iAction apiRequest)
|
||||||
|
|
||||||
-- Get the root table name with its relationships according to the Action type.
|
-- Get the root table name with its relationships according to the Action type.
|
||||||
-- This is done because of the shape of the final SQL Query. The mutation cases are wrapped in a WITH {sourceCTEName}(see Statements.hs).
|
-- This is done because of the shape of the final SQL Query. The mutation cases
|
||||||
-- So we need a FROM {sourceCTEName} instead of FROM {tableName}.
|
-- are wrapped in a WITH {sourceCTEName}(see Statements.hs). So we need a FROM
|
||||||
|
-- {sourceCTEName} instead of FROM {tableName}.
|
||||||
rootWithRels :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
|
rootWithRels :: Schema -> TableName -> [Relation] -> Action -> (QualifiedIdentifier, [Relation])
|
||||||
rootWithRels schema rootTableName allRels action = case action of
|
rootWithRels schema rootTableName allRels action = case action of
|
||||||
ActionRead _ -> (QualifiedIdentifier schema rootTableName, allRels) -- normal read case
|
ActionRead _ -> (QualifiedIdentifier schema rootTableName, allRels) -- normal read case
|
||||||
_ -> (QualifiedIdentifier mempty _sourceCTEName, mapMaybe toSourceRel allRels ++ allRels) -- mutation cases and calling proc
|
_ -> (QualifiedIdentifier mempty _sourceCTEName, mapMaybe toSourceRel allRels ++ allRels) -- mutation cases and calling proc
|
||||||
where
|
where
|
||||||
_sourceCTEName = decodeUtf8 sourceCTEName
|
_sourceCTEName = decodeUtf8 sourceCTEName
|
||||||
-- To enable embedding in the sourceCTEName cases we need to replace the foreign key tableName in the Relation
|
-- To enable embedding in the sourceCTEName cases we need to replace the
|
||||||
-- with {sourceCTEName}. This way findRel can find relationships with sourceCTEName.
|
-- foreign key tableName in the Relation with {sourceCTEName}. This way
|
||||||
|
-- findRel can find relationships with sourceCTEName.
|
||||||
toSourceRel :: Relation -> Maybe Relation
|
toSourceRel :: Relation -> Maybe Relation
|
||||||
toSourceRel r@Relation{relTable=t}
|
toSourceRel r@Relation{relTable=t}
|
||||||
| rootTableName == tableName t = Just $ r {relTable=t {tableName=_sourceCTEName}}
|
| rootTableName == tableName t = Just $ r {relTable=t {tableName=_sourceCTEName}}
|
||||||
| otherwise = Nothing
|
| otherwise = Nothing
|
||||||
|
|
||||||
-- Build the initial tree with a Depth attribute so when a self join occurs we can differentiate the parent and child tables by having
|
-- Build the initial tree with a Depth attribute so when a self join occurs we
|
||||||
-- an alias like "table_depth", this is related to http://github.com/PostgREST/postgrest/issues/987.
|
-- can differentiate the parent and child tables by having an alias like
|
||||||
|
-- "table_depth", this is related to
|
||||||
|
-- http://github.com/PostgREST/postgrest/issues/987.
|
||||||
initReadRequest :: QualifiedIdentifier -> [Tree SelectItem] -> ReadRequest
|
initReadRequest :: QualifiedIdentifier -> [Tree SelectItem] -> ReadRequest
|
||||||
initReadRequest rootQi =
|
initReadRequest rootQi =
|
||||||
foldr (treeEntry rootDepth) initial
|
foldr (treeEntry rootDepth) initial
|
||||||
@@ -114,10 +137,12 @@ addRels schema allRels parentNode (Node (query@Select{from=tbl}, (nodeName, _, a
|
|||||||
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
|
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
|
||||||
updateForest rq = addRels schema allRels rq `traverse` forest
|
updateForest rq = addRels schema allRels rq `traverse` forest
|
||||||
|
|
||||||
-- Finds a relationship between an origin and a target in the request: /origin?select=target(*)
|
-- Finds a relationship between an origin and a target in the request:
|
||||||
-- If more than one relationship is found then the request is ambiguous and we return an error.
|
-- /origin?select=target(*) If more than one relationship is found then the
|
||||||
-- In that case the request can be disambiguated by adding precision to the target or by using a hint: /origin?select=target!hint(*)
|
-- request is ambiguous and we return an error. In that case the request can
|
||||||
-- The elements will be matched according to these rules:
|
-- be disambiguated by adding precision to the target or by using a hint:
|
||||||
|
-- /origin?select=target!hint(*) The elements will be matched according to
|
||||||
|
-- these rules:
|
||||||
-- origin = table / view
|
-- origin = table / view
|
||||||
-- target = table / view / constraint / column-from-origin
|
-- target = table / view / constraint / column-from-origin
|
||||||
-- hint = table / view / constraint / column-from-origin / column-from-target
|
-- hint = table / view / constraint / column-from-origin / column-from-target
|
||||||
@@ -128,11 +153,14 @@ findRel schema allRels origin target hint =
|
|||||||
[] -> Left $ NoRelBetween origin target
|
[] -> Left $ NoRelBetween origin target
|
||||||
[r] -> Right r
|
[r] -> Right r
|
||||||
rs ->
|
rs ->
|
||||||
-- Return error if more than one relationship is found, unless we're in a self reference case.
|
-- Return error if more than one relationship is found, unless we're in a
|
||||||
|
-- self reference case.
|
||||||
--
|
--
|
||||||
-- Here we handle a self reference relationship to not cause a breaking change:
|
-- Here we handle a self reference relationship to not cause a breaking
|
||||||
-- In a self reference we get two relationships with the same foreign key and relTable/relFtable but with different cardinalities(m2o/o2m)
|
-- change: In a self reference we get two relationships with the same
|
||||||
-- We output the O2M rel, the M2O rel can be obtained by using the origin column as an embed hint.
|
-- foreign key and relTable/relFtable but with different
|
||||||
|
-- cardinalities(m2o/o2m) We output the O2M rel, the M2O rel can be
|
||||||
|
-- obtained by using the origin column as an embed hint.
|
||||||
let [rel0, rel1] = take 2 rs in
|
let [rel0, rel1] = take 2 rs in
|
||||||
if length rs == 2 && relLink rel0 == relLink rel1 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
|
if length rs == 2 && relLink rel0 == relLink rel1 && relTable rel0 == relTable rel1 && relFTable rel0 == relFTable rel1
|
||||||
then note (NoRelBetween origin target) (find (\r -> relType r == O2M) rs)
|
then note (NoRelBetween origin target) (find (\r -> relType r == O2M) rs)
|
||||||
@@ -189,7 +217,7 @@ addJoinConditions previousAlias (Node node@(query@Select{from=tbl}, nodeProps@(_
|
|||||||
Just r -> Node (augmentQuery r, nodeProps) <$> updatedForest
|
Just r -> Node (augmentQuery r, nodeProps) <$> updatedForest
|
||||||
Nothing -> Node node <$> updatedForest
|
Nothing -> Node node <$> updatedForest
|
||||||
where
|
where
|
||||||
newAlias = case isSelfReference <$> rel of
|
newAlias = case Relation.isSelfReference <$> rel of
|
||||||
Just True
|
Just True
|
||||||
| depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased
|
| depth /= 0 -> Just (qiName tbl <> "_" <> show depth) -- root node doesn't get aliased
|
||||||
| otherwise -> Nothing
|
| otherwise -> Nothing
|
||||||
@@ -217,9 +245,10 @@ getJoinConditions previousAlias newAlias (Relation Table{tableSchema=tSchema, ta
|
|||||||
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, colName c)
|
JoinCondition (maybe qi1 (QualifiedIdentifier mempty) previousAlias, colName c)
|
||||||
(maybe qi2 (QualifiedIdentifier mempty) newAlias, colName fc)
|
(maybe qi2 (QualifiedIdentifier mempty) newAlias, colName fc)
|
||||||
|
|
||||||
-- On mutation and calling proc cases we wrap the target table in a WITH {sourceCTEName}
|
-- On mutation and calling proc cases we wrap the target table in a WITH
|
||||||
-- if this happens remove the schema `FROM "schema"."{sourceCTEName}"` and use only the
|
-- {sourceCTEName} if this happens remove the schema `FROM
|
||||||
-- `FROM "{sourceCTEName}"`. If the schema remains the FROM would be invalid.
|
-- "schema"."{sourceCTEName}"` and use only the `FROM "{sourceCTEName}"`.
|
||||||
|
-- If the schema remains the FROM would be invalid.
|
||||||
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
|
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
|
||||||
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == decodeUtf8 sourceCTEName then mempty else schema) tbl
|
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == decodeUtf8 sourceCTEName then mempty else schema) tbl
|
||||||
|
|
||||||
@@ -318,16 +347,19 @@ mutateRequest schema tName apiRequest pkCols readReq = mapLeft ApiRequestError $
|
|||||||
|
|
||||||
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
|
returningCols :: ReadRequest -> [FieldName] -> [FieldName]
|
||||||
returningCols rr@(Node _ forest) pkCols
|
returningCols rr@(Node _ forest) pkCols
|
||||||
-- if * is part of the select, we must not add pk or fk columns manually - otherwise those would be selected and output twice
|
-- if * is part of the select, we must not add pk or fk columns manually -
|
||||||
|
-- otherwise those would be selected and output twice
|
||||||
| "*" `elem` fldNames = ["*"]
|
| "*" `elem` fldNames = ["*"]
|
||||||
| otherwise = returnings
|
| otherwise = returnings
|
||||||
where
|
where
|
||||||
fldNames = fstFieldNames rr
|
fldNames = fstFieldNames rr
|
||||||
-- Without fkCols, when a mutateRequest to /projects?select=name,clients(name) occurs, the RETURNING SQL part would be
|
-- Without fkCols, when a mutateRequest to
|
||||||
-- `RETURNING name`(see QueryBuilder).
|
-- /projects?select=name,clients(name) occurs, the RETURNING SQL part would
|
||||||
-- This would make the embedding fail because the following JOIN would need the "client_id" column from projects.
|
-- be `RETURNING name`(see QueryBuilder). This would make the embedding
|
||||||
-- So this adds the foreign key columns to ensure the embedding succeeds, result would be `RETURNING name, client_id`.
|
-- fail because the following JOIN would need the "client_id" column from
|
||||||
-- This also works for the other relType's.
|
-- projects. So this adds the foreign key columns to ensure the embedding
|
||||||
|
-- succeeds, result would be `RETURNING name, client_id`. This also works
|
||||||
|
-- for the other relType's.
|
||||||
fkCols = concat $ mapMaybe (\case
|
fkCols = concat $ mapMaybe (\case
|
||||||
Node (_, (_, Just Relation{relColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
|
Node (_, (_, Just Relation{relColumns=cols, relType=relTyp}, _, _, _)) _ -> case relTyp of
|
||||||
O2M -> Just cols
|
O2M -> Just cols
|
||||||
@@ -335,10 +367,13 @@ returningCols rr@(Node _ forest) pkCols
|
|||||||
M2M -> Just cols
|
M2M -> Just cols
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
) forest
|
) forest
|
||||||
-- However if the "client_id" is present, e.g. mutateRequest to /projects?select=client_id,name,clients(name)
|
-- However if the "client_id" is present, e.g. mutateRequest to
|
||||||
-- we would get `RETURNING client_id, name, client_id` and then we would produce the "column reference \"client_id\" is ambiguous"
|
-- /projects?select=client_id,name,clients(name) we would get `RETURNING
|
||||||
-- error from PostgreSQL. So we deduplicate with Set:
|
-- client_id, name, client_id` and then we would produce the "column
|
||||||
-- We are adding the primary key columns as well to make sure, that a proper location header can always be built for INSERT/POST
|
-- reference \"client_id\" is ambiguous" error from PostgreSQL. So we
|
||||||
|
-- deduplicate with Set: We are adding the primary key columns as well to
|
||||||
|
-- make sure, that a proper location header can always be built for
|
||||||
|
-- INSERT/POST
|
||||||
returnings = S.toList . S.fromList $ fldNames ++ (colName <$> fkCols) ++ pkCols
|
returnings = S.toList . S.fromList $ fldNames ++ (colName <$> fkCols) ++ pkCols
|
||||||
|
|
||||||
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
|
||||||
@@ -1,10 +1,26 @@
|
|||||||
{-|
|
{-|
|
||||||
Module : PostgREST.Parsers
|
Module : PostgREST.Request.Parsers
|
||||||
Description : PostgREST parser combinators
|
Description : PostgREST parser combinators
|
||||||
|
|
||||||
This module is in charge of parsing all the querystring values in an url, e.g. the select, id, order in `/projects?select=id,name&id=eq.1&order=id,name.desc`.
|
This module is in charge of parsing all the querystring values in an url, e.g. the select, id, order in `/projects?select=id,name&id=eq.1&order=id,name.desc`.
|
||||||
-}
|
-}
|
||||||
module PostgREST.Parsers where
|
module PostgREST.Request.Parsers
|
||||||
|
( pColumns
|
||||||
|
, pLogicPath
|
||||||
|
, pLogicSingleVal
|
||||||
|
, pLogicTree
|
||||||
|
, pOrder
|
||||||
|
, pOrderTerm
|
||||||
|
, pRequestColumns
|
||||||
|
, pRequestFilter
|
||||||
|
, pRequestLogicTree
|
||||||
|
, pRequestOnConflict
|
||||||
|
, pRequestOrder
|
||||||
|
, pRequestRange
|
||||||
|
, pRequestSelect
|
||||||
|
, pSingleVal
|
||||||
|
, pTreePath
|
||||||
|
) where
|
||||||
|
|
||||||
import qualified Data.HashMap.Strict as M
|
import qualified Data.HashMap.Strict as M
|
||||||
import qualified Data.Set as S
|
import qualified Data.Set as S
|
||||||
@@ -13,17 +29,25 @@ import Data.Either.Combinators (mapLeft)
|
|||||||
import Data.Foldable (foldl1)
|
import Data.Foldable (foldl1)
|
||||||
import Data.List (init, last)
|
import Data.List (init, last)
|
||||||
import Data.Text (intercalate, replace, strip)
|
import Data.Text (intercalate, replace, strip)
|
||||||
import Text.Read (read)
|
import Data.Tree (Tree (..))
|
||||||
|
import Text.Parsec.Error (errorMessages,
|
||||||
import Data.Tree
|
showErrorMessages)
|
||||||
import Text.Parsec.Error
|
import Text.ParserCombinators.Parsec (GenParser, ParseError, Parser,
|
||||||
import Text.ParserCombinators.Parsec hiding (many, (<|>))
|
anyChar, between, char, digit,
|
||||||
|
eof, errorPos, letter,
|
||||||
|
lookAhead, many1, noneOf,
|
||||||
|
notFollowedBy, oneOf, option,
|
||||||
|
optionMaybe, parse, sepBy1,
|
||||||
|
string, try, (<?>))
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Identifiers (FieldName)
|
||||||
import PostgREST.Error (ApiRequestError (ParseRequestError))
|
import PostgREST.Error (ApiRequestError (ParseRequestError))
|
||||||
|
import PostgREST.Query.SqlFragment (ftsOperators, operators)
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
import PostgREST.Types
|
|
||||||
import Protolude hiding (intercalate, option, replace, toS,
|
import PostgREST.Request.Types
|
||||||
try)
|
|
||||||
|
import Protolude hiding (intercalate, option, replace, toS, try)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
|
pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
|
||||||
@@ -250,23 +274,3 @@ mapError = mapLeft translateError
|
|||||||
message = show $ errorPos e
|
message = show $ errorPos e
|
||||||
details = strip $ replace "\n" " " $ toS
|
details = strip $ replace "\n" " " $ toS
|
||||||
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
|
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
|
||||||
|
|
||||||
-- Used for the config value "role-claim-key"
|
|
||||||
pRoleClaimKey :: Text -> Either Text JSPath
|
|
||||||
pRoleClaimKey selStr =
|
|
||||||
mapLeft show $ parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
|
|
||||||
|
|
||||||
pJSPath :: Parser JSPath
|
|
||||||
pJSPath = toJSPath <$> (period *> pPath `sepBy` period <* eof)
|
|
||||||
where
|
|
||||||
toJSPath :: [(Text, Maybe Int)] -> JSPath
|
|
||||||
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
|
|
||||||
period = char '.' <?> "period (.)"
|
|
||||||
pPath :: Parser (Text, Maybe Int)
|
|
||||||
pPath = (,) <$> pJSPKey <*> optionMaybe pJSPIdx
|
|
||||||
|
|
||||||
pJSPKey :: Parser Text
|
|
||||||
pJSPKey = toS <$> many1 (alphaNum <|> oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
|
|
||||||
|
|
||||||
pJSPIdx :: Parser Int
|
|
||||||
pJSPIdx = char '[' *> (read <$> many1 digit) <* char ']' <?> "array index [0..n]"
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
module PostgREST.Request.Preferences where
|
||||||
|
|
||||||
|
import GHC.Show
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
data PreferResolution
|
||||||
|
= MergeDuplicates
|
||||||
|
| IgnoreDuplicates
|
||||||
|
|
||||||
|
instance Show PreferResolution where
|
||||||
|
show MergeDuplicates = "resolution=merge-duplicates"
|
||||||
|
show IgnoreDuplicates = "resolution=ignore-duplicates"
|
||||||
|
|
||||||
|
-- | How to return the mutated data. From https://tools.ietf.org/html/rfc7240#section-4.2
|
||||||
|
data PreferRepresentation
|
||||||
|
= Full -- ^ Return the body plus the Location header(in case of POST).
|
||||||
|
| HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.
|
||||||
|
| None -- ^ Return nothing from the mutated data.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance Show PreferRepresentation where
|
||||||
|
show Full = "return=representation"
|
||||||
|
show None = "return=minimal"
|
||||||
|
show HeadersOnly = mempty
|
||||||
|
|
||||||
|
data PreferParameters
|
||||||
|
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure
|
||||||
|
| MultipleObjects -- ^ Pass an array of json objects as params to a stored procedure
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance Show PreferParameters where
|
||||||
|
show SingleObject = "params=single-object"
|
||||||
|
show MultipleObjects = "params=multiple-objects"
|
||||||
|
|
||||||
|
data PreferCount
|
||||||
|
= ExactCount -- ^ exact count(slower)
|
||||||
|
| PlannedCount -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.
|
||||||
|
| EstimatedCount -- ^ use the query planner rows if the count is superior to max-rows, otherwise get the exact count.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance Show PreferCount where
|
||||||
|
show ExactCount = "count=exact"
|
||||||
|
show PlannedCount = "count=planned"
|
||||||
|
show EstimatedCount = "count=estimated"
|
||||||
|
|
||||||
|
data PreferTransaction
|
||||||
|
= Commit -- Commit transaction - the default.
|
||||||
|
| Rollback -- Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance Show PreferTransaction where
|
||||||
|
show Commit = "tx=commit"
|
||||||
|
show Rollback = "tx=rollback"
|
||||||
@@ -0,0 +1,211 @@
|
|||||||
|
{-# LANGUAGE DuplicateRecordFields #-}
|
||||||
|
module PostgREST.Request.Types
|
||||||
|
( Alias
|
||||||
|
, Depth
|
||||||
|
, EmbedHint
|
||||||
|
, EmbedPath
|
||||||
|
, Field
|
||||||
|
, Filter(..)
|
||||||
|
, JoinCondition(..)
|
||||||
|
, JsonOperand(..)
|
||||||
|
, JsonOperation(..)
|
||||||
|
, JsonPath
|
||||||
|
, ListVal
|
||||||
|
, LogicOperator(..)
|
||||||
|
, LogicTree(..)
|
||||||
|
, MutateQuery(..)
|
||||||
|
, MutateRequest
|
||||||
|
, NodeName
|
||||||
|
, OpExpr(..)
|
||||||
|
, Operation (..)
|
||||||
|
, OrderDirection(..)
|
||||||
|
, OrderNulls(..)
|
||||||
|
, OrderTerm(..)
|
||||||
|
, ReadNode
|
||||||
|
, ReadQuery(..)
|
||||||
|
, ReadRequest
|
||||||
|
, SelectItem
|
||||||
|
, SingleVal
|
||||||
|
, fstFieldNames
|
||||||
|
) where
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
import qualified Data.Set as S
|
||||||
|
|
||||||
|
import Data.Tree (Tree (..))
|
||||||
|
|
||||||
|
import qualified GHC.Show (show)
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Identifiers (FieldName,
|
||||||
|
QualifiedIdentifier)
|
||||||
|
import PostgREST.DbStructure.Relation (Relation)
|
||||||
|
import PostgREST.RangeQuery (NonnegRange)
|
||||||
|
import PostgREST.Request.Preferences (PreferResolution)
|
||||||
|
|
||||||
|
import Protolude
|
||||||
|
|
||||||
|
|
||||||
|
type ReadRequest = Tree ReadNode
|
||||||
|
type MutateRequest = MutateQuery
|
||||||
|
|
||||||
|
type ReadNode =
|
||||||
|
(ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe EmbedHint, Depth))
|
||||||
|
|
||||||
|
type NodeName = Text
|
||||||
|
type Depth = Integer
|
||||||
|
|
||||||
|
data ReadQuery = Select
|
||||||
|
{ select :: [SelectItem]
|
||||||
|
, from :: QualifiedIdentifier
|
||||||
|
-- ^ A table alias is used in case of self joins
|
||||||
|
, fromAlias :: Maybe Alias
|
||||||
|
-- ^ Only used for Many to Many joins. Parent and Child joins use explicit joins.
|
||||||
|
, implicitJoins :: [QualifiedIdentifier]
|
||||||
|
, where_ :: [LogicTree]
|
||||||
|
, joinConditions :: [JoinCondition]
|
||||||
|
, order :: [OrderTerm]
|
||||||
|
, range_ :: NonnegRange
|
||||||
|
}
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
data JoinCondition =
|
||||||
|
JoinCondition
|
||||||
|
(QualifiedIdentifier, FieldName)
|
||||||
|
(QualifiedIdentifier, FieldName)
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
data OrderTerm = OrderTerm
|
||||||
|
{ otTerm :: Field
|
||||||
|
, otDirection :: Maybe OrderDirection
|
||||||
|
, otNullOrder :: Maybe OrderNulls
|
||||||
|
}
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
data OrderDirection
|
||||||
|
= OrderAsc
|
||||||
|
| OrderDesc
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
instance Show OrderDirection where
|
||||||
|
show OrderAsc = "ASC"
|
||||||
|
show OrderDesc = "DESC"
|
||||||
|
|
||||||
|
data OrderNulls
|
||||||
|
= OrderNullsFirst
|
||||||
|
| OrderNullsLast
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
instance Show OrderNulls where
|
||||||
|
show OrderNullsFirst = "NULLS FIRST"
|
||||||
|
show OrderNullsLast = "NULLS LAST"
|
||||||
|
|
||||||
|
data MutateQuery
|
||||||
|
= Insert
|
||||||
|
{ in_ :: QualifiedIdentifier
|
||||||
|
, insCols :: S.Set FieldName
|
||||||
|
, insBody :: Maybe BL.ByteString
|
||||||
|
, onConflict :: Maybe (PreferResolution, [FieldName])
|
||||||
|
, where_ :: [LogicTree]
|
||||||
|
, returning :: [FieldName]
|
||||||
|
}
|
||||||
|
| Update
|
||||||
|
{ in_ :: QualifiedIdentifier
|
||||||
|
, updCols :: S.Set FieldName
|
||||||
|
, updBody :: Maybe BL.ByteString
|
||||||
|
, where_ :: [LogicTree]
|
||||||
|
, returning :: [FieldName]
|
||||||
|
}
|
||||||
|
| Delete
|
||||||
|
{ in_ :: QualifiedIdentifier
|
||||||
|
, where_ :: [LogicTree]
|
||||||
|
, returning :: [FieldName]
|
||||||
|
}
|
||||||
|
|
||||||
|
-- | This type will hold information about which particular 'Relation' between
|
||||||
|
-- two tables to choose when there are multiple ones.
|
||||||
|
-- Specifically, it will contain the name of the foreign key or the join table
|
||||||
|
-- in many to many relations.
|
||||||
|
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
||||||
|
|
||||||
|
type Field = (FieldName, JsonPath)
|
||||||
|
type Cast = Text
|
||||||
|
type Alias = Text
|
||||||
|
|
||||||
|
-- | Disambiguates an embedding operation when there's multiple relationships
|
||||||
|
-- between two tables. Can be the name of a foreign key constraint, column
|
||||||
|
-- name or the junction in an m2m relationship.
|
||||||
|
type EmbedHint = Text
|
||||||
|
|
||||||
|
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path
|
||||||
|
-- ["clients", "projects"]
|
||||||
|
type EmbedPath = [Text]
|
||||||
|
|
||||||
|
-- | Json path operations as specified in
|
||||||
|
-- https://www.postgresql.org/docs/current/static/functions-json.html
|
||||||
|
type JsonPath = [JsonOperation]
|
||||||
|
|
||||||
|
-- | Represents the single arrow `->` or double arrow `->>` operators
|
||||||
|
data JsonOperation
|
||||||
|
= JArrow { jOp :: JsonOperand }
|
||||||
|
| J2Arrow { jOp :: JsonOperand }
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text
|
||||||
|
-- because we reuse our escaping functons and let pg do the casting with
|
||||||
|
-- '1'::int
|
||||||
|
data JsonOperand
|
||||||
|
= JKey { jVal :: Text }
|
||||||
|
| JIdx { jVal :: Text }
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
||||||
|
fstFieldNames :: ReadRequest -> [FieldName]
|
||||||
|
fstFieldNames (Node (sel, _) _) =
|
||||||
|
fst . (\(f, _, _, _) -> f) <$> select sel
|
||||||
|
|
||||||
|
|
||||||
|
-- | Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
|
||||||
|
--
|
||||||
|
-- And
|
||||||
|
-- / \
|
||||||
|
-- name.eq.N Or
|
||||||
|
-- / \
|
||||||
|
-- id.eq.1 id.eq.2
|
||||||
|
data LogicTree
|
||||||
|
= Expr Bool LogicOperator [LogicTree]
|
||||||
|
| Stmnt Filter
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
data LogicOperator
|
||||||
|
= And
|
||||||
|
| Or
|
||||||
|
deriving Eq
|
||||||
|
|
||||||
|
instance Show LogicOperator where
|
||||||
|
show And = "AND"
|
||||||
|
show Or = "OR"
|
||||||
|
|
||||||
|
data Filter = Filter
|
||||||
|
{ field :: Field
|
||||||
|
, opExpr :: OpExpr
|
||||||
|
}
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
data OpExpr =
|
||||||
|
OpExpr Bool Operation
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
data Operation
|
||||||
|
= Op Operator SingleVal
|
||||||
|
| In ListVal
|
||||||
|
| Fts Operator (Maybe Language) SingleVal
|
||||||
|
deriving (Eq)
|
||||||
|
|
||||||
|
type Operator = Text
|
||||||
|
type Language = Text
|
||||||
|
|
||||||
|
-- | Represents a single value in a filter, e.g. id=eq.singleval
|
||||||
|
type SingleVal = Text
|
||||||
|
|
||||||
|
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
||||||
|
type ListVal = [Text]
|
||||||
@@ -1,580 +0,0 @@
|
|||||||
{-|
|
|
||||||
Module : PostgREST.Types
|
|
||||||
Description : PostgREST common types and functions used by the rest of the modules
|
|
||||||
-}
|
|
||||||
{-# LANGUAGE DeriveAnyClass #-}
|
|
||||||
{-# LANGUAGE DeriveGeneric #-}
|
|
||||||
{-# LANGUAGE DuplicateRecordFields #-}
|
|
||||||
|
|
||||||
module PostgREST.Types where
|
|
||||||
|
|
||||||
import Control.Lens.Getter (view)
|
|
||||||
import Control.Lens.Tuple (_1)
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.ByteString as BS
|
|
||||||
import qualified Data.ByteString.Internal as BS (c2w)
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
import qualified Data.HashMap.Strict as M
|
|
||||||
import qualified Data.Set as S
|
|
||||||
import qualified GHC.Show
|
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, hContentType)
|
|
||||||
|
|
||||||
import Data.Tree
|
|
||||||
|
|
||||||
import PostgREST.RangeQuery (NonnegRange)
|
|
||||||
import Protolude hiding (toS)
|
|
||||||
import Protolude.Conv (toS)
|
|
||||||
|
|
||||||
-- | Enumeration of currently supported response content types
|
|
||||||
data ContentType = CTApplicationJSON | CTSingularJSON
|
|
||||||
| CTTextCSV | CTTextPlain
|
|
||||||
| CTOpenAPI | CTUrlEncoded | CTOctetStream
|
|
||||||
| CTAny | CTOther ByteString deriving (Eq)
|
|
||||||
|
|
||||||
-- | Convert from ContentType to a full HTTP Header
|
|
||||||
toHeader :: ContentType -> Header
|
|
||||||
toHeader ct = (hContentType, toMime ct <> charset)
|
|
||||||
where
|
|
||||||
charset = case ct of
|
|
||||||
CTOctetStream -> mempty
|
|
||||||
CTOther _ -> mempty
|
|
||||||
_ -> "; charset=utf-8"
|
|
||||||
|
|
||||||
-- | Convert from ContentType to a ByteString representing the mime type
|
|
||||||
toMime :: ContentType -> ByteString
|
|
||||||
toMime CTApplicationJSON = "application/json"
|
|
||||||
toMime CTTextCSV = "text/csv"
|
|
||||||
toMime CTTextPlain = "text/plain"
|
|
||||||
toMime CTOpenAPI = "application/openapi+json"
|
|
||||||
toMime CTSingularJSON = "application/vnd.pgrst.object+json"
|
|
||||||
toMime CTUrlEncoded = "application/x-www-form-urlencoded"
|
|
||||||
toMime CTOctetStream = "application/octet-stream"
|
|
||||||
toMime CTAny = "*/*"
|
|
||||||
toMime (CTOther ct) = ct
|
|
||||||
|
|
||||||
-- | Convert from ByteString to ContentType. Warning: discards MIME parameters
|
|
||||||
decodeContentType :: BS.ByteString -> ContentType
|
|
||||||
decodeContentType ct = case BS.takeWhile (/= BS.c2w ';') ct of
|
|
||||||
"application/json" -> CTApplicationJSON
|
|
||||||
"text/csv" -> CTTextCSV
|
|
||||||
"text/plain" -> CTTextPlain
|
|
||||||
"application/openapi+json" -> CTOpenAPI
|
|
||||||
"application/vnd.pgrst.object+json" -> CTSingularJSON
|
|
||||||
"application/vnd.pgrst.object" -> CTSingularJSON
|
|
||||||
"application/x-www-form-urlencoded" -> CTUrlEncoded
|
|
||||||
"application/octet-stream" -> CTOctetStream
|
|
||||||
"*/*" -> CTAny
|
|
||||||
ct' -> CTOther ct'
|
|
||||||
|
|
||||||
-- | A SQL query that can be executed independently
|
|
||||||
type SqlQuery = ByteString
|
|
||||||
|
|
||||||
-- | A part of a SQL query that cannot be executed independently
|
|
||||||
type SqlFragment = ByteString
|
|
||||||
|
|
||||||
data PreferResolution = MergeDuplicates | IgnoreDuplicates
|
|
||||||
instance Show PreferResolution where
|
|
||||||
show MergeDuplicates = "resolution=merge-duplicates"
|
|
||||||
show IgnoreDuplicates = "resolution=ignore-duplicates"
|
|
||||||
|
|
||||||
-- | How to return the mutated data. From https://tools.ietf.org/html/rfc7240#section-4.2
|
|
||||||
data PreferRepresentation = Full -- ^ Return the body plus the Location header(in case of POST).
|
|
||||||
| HeadersOnly -- ^ Return the Location header(in case of POST). This needs a SELECT privilege on the pk.
|
|
||||||
| None -- ^ Return nothing from the mutated data.
|
|
||||||
deriving Eq
|
|
||||||
instance Show PreferRepresentation where
|
|
||||||
show Full = "return=representation"
|
|
||||||
show None = "return=minimal"
|
|
||||||
show HeadersOnly = mempty
|
|
||||||
|
|
||||||
data PreferParameters
|
|
||||||
= SingleObject -- ^ Pass all parameters as a single json object to a stored procedure
|
|
||||||
| MultipleObjects -- ^ Pass an array of json objects as params to a stored procedure
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
instance Show PreferParameters where
|
|
||||||
show SingleObject = "params=single-object"
|
|
||||||
show MultipleObjects = "params=multiple-objects"
|
|
||||||
|
|
||||||
data PreferCount
|
|
||||||
= ExactCount -- ^ exact count(slower)
|
|
||||||
| PlannedCount -- ^ PostgreSQL query planner rows count guess. Done by using EXPLAIN {query}.
|
|
||||||
| EstimatedCount -- ^ use the query planner rows if the count is superior to max-rows, otherwise get the exact count.
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
instance Show PreferCount where
|
|
||||||
show ExactCount = "count=exact"
|
|
||||||
show PlannedCount = "count=planned"
|
|
||||||
show EstimatedCount = "count=estimated"
|
|
||||||
|
|
||||||
data PreferTransaction
|
|
||||||
= Commit -- Commit transaction - the default.
|
|
||||||
| Rollback -- Rollback transaction after sending the response - does not persist changes, e.g. for running tests.
|
|
||||||
deriving Eq
|
|
||||||
|
|
||||||
instance Show PreferTransaction where
|
|
||||||
show Commit = "tx=commit"
|
|
||||||
show Rollback = "tx=rollback"
|
|
||||||
|
|
||||||
data DbStructure = DbStructure {
|
|
||||||
dbTables :: [Table]
|
|
||||||
, dbColumns :: [Column]
|
|
||||||
, dbRelations :: [Relation]
|
|
||||||
, dbPrimaryKeys :: [PrimaryKey]
|
|
||||||
, dbProcs :: ProcsMap
|
|
||||||
, pgVersion :: PgVersion
|
|
||||||
} deriving (Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
-- TODO Table could hold references to all its Columns
|
|
||||||
tableCols :: DbStructure -> Schema -> TableName -> [Column]
|
|
||||||
tableCols dbs tSchema tName = filter (\Column{colTable=Table{tableSchema=s, tableName=t}} -> s==tSchema && t==tName) $ dbColumns dbs
|
|
||||||
|
|
||||||
-- TODO Table could hold references to all its PrimaryKeys
|
|
||||||
tablePKCols :: DbStructure -> Schema -> TableName -> [Text]
|
|
||||||
tablePKCols dbs tSchema tName = pkName <$> filter (\pk -> tSchema == (tableSchema . pkTable) pk && tName == (tableName . pkTable) pk) (dbPrimaryKeys dbs)
|
|
||||||
|
|
||||||
data PgArg = PgArg {
|
|
||||||
pgaName :: Text
|
|
||||||
, pgaType :: Text
|
|
||||||
, pgaReq :: Bool
|
|
||||||
, pgaVar :: Bool
|
|
||||||
} deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data PgType = Scalar | Composite QualifiedIdentifier deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data RetType = Single PgType | SetOf PgType deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data ProcVolatility = Volatile | Stable | Immutable
|
|
||||||
deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data ProcDescription = ProcDescription {
|
|
||||||
pdSchema :: Schema
|
|
||||||
, pdName :: Text
|
|
||||||
, pdDescription :: Maybe Text
|
|
||||||
, pdArgs :: [PgArg]
|
|
||||||
, pdReturnType :: RetType
|
|
||||||
, pdVolatility :: ProcVolatility
|
|
||||||
, pdHasVariadic :: Bool
|
|
||||||
} deriving (Eq, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
-- Order by least number of args in the case of overloaded functions
|
|
||||||
instance Ord ProcDescription where
|
|
||||||
ProcDescription schema1 name1 des1 args1 rt1 vol1 hasVar1 `compare` ProcDescription schema2 name2 des2 args2 rt2 vol2 hasVar2
|
|
||||||
| schema1 == schema2 && name1 == name2 && length args1 < length args2 = LT
|
|
||||||
| schema2 == schema2 && name1 == name2 && length args1 > length args2 = GT
|
|
||||||
| otherwise = (schema1, name1, des1, args1, rt1, vol1, hasVar1) `compare` (schema2, name2, des2, args2, rt2, vol2, hasVar2)
|
|
||||||
|
|
||||||
-- | A map of all procs, all of which can be overloaded(one entry will have more than one ProcDescription).
|
|
||||||
-- | It uses a HashMap for a faster lookup.
|
|
||||||
type ProcsMap = M.HashMap QualifiedIdentifier [ProcDescription]
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Search a pg procedure by its parameters. Since a function can be overloaded, the name is not enough to find it.
|
|
||||||
An overloaded function can have a different volatility or even a different return type.
|
|
||||||
Ideally, handling overloaded functions should be left to pg itself. But we need to know certain proc attributes in advance.
|
|
||||||
-}
|
|
||||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> ProcDescription
|
|
||||||
findProc qi payloadKeys paramsAsSingleObject allProcs = fromMaybe fallback bestMatch
|
|
||||||
where
|
|
||||||
-- instead of passing Maybe ProcDescription around, we create a fallback description here when we can't find a matching function
|
|
||||||
-- args is empty, but because "specifiedProcArgs" will fill the missing arguments with default type text, this is not a problem
|
|
||||||
fallback = ProcDescription (qiSchema qi) (qiName qi) Nothing mempty (SetOf $ Composite $ QualifiedIdentifier mempty "record") Volatile False
|
|
||||||
bestMatch =
|
|
||||||
case M.lookup qi allProcs of
|
|
||||||
Nothing -> Nothing
|
|
||||||
Just [proc] -> Just proc -- if it's not an overloaded function then immediately get the ProcDescription
|
|
||||||
Just procs -> find matches procs -- Handle overloaded functions case
|
|
||||||
matches proc =
|
|
||||||
if paramsAsSingleObject
|
|
||||||
-- if the arg is not of json type let the db give the err
|
|
||||||
then length (pdArgs proc) == 1
|
|
||||||
else payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> pdArgs proc)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Search the procedure parameters by matching them with the specified keys.
|
|
||||||
If the key doesn't match a parameter, a parameter with a default type "text" is assumed.
|
|
||||||
-}
|
|
||||||
specifiedProcArgs :: S.Set FieldName -> ProcDescription -> [PgArg]
|
|
||||||
specifiedProcArgs keys proc =
|
|
||||||
(\k -> fromMaybe (PgArg k "text" True False) (find ((==) k . pgaName) (pdArgs proc))) <$> S.toList keys
|
|
||||||
|
|
||||||
procReturnsScalar :: ProcDescription -> Bool
|
|
||||||
procReturnsScalar proc = case proc of
|
|
||||||
ProcDescription{pdReturnType = (Single Scalar)} -> True
|
|
||||||
ProcDescription{pdReturnType = (SetOf Scalar)} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
procReturnsSingle :: ProcDescription -> Bool
|
|
||||||
procReturnsSingle proc = case proc of
|
|
||||||
ProcDescription{pdReturnType = (Single _)} -> True
|
|
||||||
_ -> False
|
|
||||||
|
|
||||||
procTableName :: ProcDescription -> Maybe TableName
|
|
||||||
procTableName proc = case pdReturnType proc of
|
|
||||||
SetOf (Composite qi) -> Just $ qiName qi
|
|
||||||
Single (Composite qi) -> Just $ qiName qi
|
|
||||||
_ -> Nothing
|
|
||||||
|
|
||||||
type Schema = Text
|
|
||||||
type TableName = Text
|
|
||||||
|
|
||||||
data Table = Table {
|
|
||||||
tableSchema :: Schema
|
|
||||||
, tableName :: TableName
|
|
||||||
, tableDescription :: Maybe Text
|
|
||||||
, tableInsertable :: Bool
|
|
||||||
} deriving (Show, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
instance Eq Table where
|
|
||||||
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
|
|
||||||
|
|
||||||
tableQi :: Table -> QualifiedIdentifier
|
|
||||||
tableQi Table{tableSchema=s, tableName=n} = QualifiedIdentifier s n
|
|
||||||
|
|
||||||
newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Eq, Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data Column =
|
|
||||||
Column {
|
|
||||||
colTable :: Table
|
|
||||||
, colName :: FieldName
|
|
||||||
, colDescription :: Maybe Text
|
|
||||||
, colNullable :: Bool
|
|
||||||
, colType :: Text
|
|
||||||
, colMaxLen :: Maybe Int32
|
|
||||||
, colDefault :: Maybe Text
|
|
||||||
, colEnum :: [Text]
|
|
||||||
, colFK :: Maybe ForeignKey
|
|
||||||
} deriving (Ord, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
instance Eq Column where
|
|
||||||
Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2
|
|
||||||
|
|
||||||
-- | The source table column a view column refers to
|
|
||||||
type SourceColumn = (Column, ViewColumn)
|
|
||||||
type ViewColumn = Column
|
|
||||||
|
|
||||||
data PrimaryKey = PrimaryKey {
|
|
||||||
pkTable :: Table
|
|
||||||
, pkName :: Text
|
|
||||||
} deriving (Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
data OrderDirection = OrderAsc | OrderDesc deriving (Eq)
|
|
||||||
instance Show OrderDirection where
|
|
||||||
show OrderAsc = "ASC"
|
|
||||||
show OrderDesc = "DESC"
|
|
||||||
|
|
||||||
data OrderNulls = OrderNullsFirst | OrderNullsLast deriving (Eq)
|
|
||||||
instance Show OrderNulls where
|
|
||||||
show OrderNullsFirst = "NULLS FIRST"
|
|
||||||
show OrderNullsLast = "NULLS LAST"
|
|
||||||
|
|
||||||
data OrderTerm = OrderTerm {
|
|
||||||
otTerm :: Field
|
|
||||||
, otDirection :: Maybe OrderDirection
|
|
||||||
, otNullOrder :: Maybe OrderNulls
|
|
||||||
} deriving (Eq)
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Represents a pg identifier with a prepended schema name "schema.table"
|
|
||||||
When qiSchema is "", the schema is defined by the pg search_path
|
|
||||||
-}
|
|
||||||
data QualifiedIdentifier = QualifiedIdentifier {
|
|
||||||
qiSchema :: Schema
|
|
||||||
, qiName :: TableName
|
|
||||||
} deriving (Eq, Ord, Generic, JSON.ToJSON, JSON.ToJSONKey)
|
|
||||||
instance Hashable QualifiedIdentifier
|
|
||||||
|
|
||||||
-- | The relationship [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
|
|
||||||
-- | TODO: missing one-to-one
|
|
||||||
data Cardinality = O2M -- ^ one-to-many, previously known as Parent
|
|
||||||
| M2O -- ^ many-to-one, previously known as Child
|
|
||||||
| M2M -- ^ many-to-many, previously known as Many
|
|
||||||
deriving (Eq, Generic, JSON.ToJSON)
|
|
||||||
instance Show Cardinality where
|
|
||||||
show O2M = "o2m"
|
|
||||||
show M2O = "m2o"
|
|
||||||
show M2M = "m2m"
|
|
||||||
|
|
||||||
{-|
|
|
||||||
"Relation"ship between two tables.
|
|
||||||
The order of the relColumns and relFColumns should be maintained to get the join conditions right.
|
|
||||||
TODO merge relColumns and relFColumns to a tuple or Data.Bimap
|
|
||||||
-}
|
|
||||||
data Relation = Relation {
|
|
||||||
relTable :: Table
|
|
||||||
, relColumns :: [Column]
|
|
||||||
, relFTable :: Table
|
|
||||||
, relFColumns :: [Column]
|
|
||||||
, relType :: Cardinality
|
|
||||||
, relLink :: Link -- ^ Constraint on O2M/M2O, Junction for M2M Cardinality
|
|
||||||
} deriving (Eq, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
type ConstraintName = Text
|
|
||||||
|
|
||||||
-- | Junction table on an M2M relationship
|
|
||||||
data Link
|
|
||||||
= Constraint { constName :: ConstraintName }
|
|
||||||
| Junction {
|
|
||||||
junTable :: Table
|
|
||||||
, junLink1 :: Link
|
|
||||||
, junCols1 :: [Column]
|
|
||||||
, junLink2 :: Link
|
|
||||||
, junCols2 :: [Column]
|
|
||||||
}
|
|
||||||
deriving (Eq, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
isSelfReference :: Relation -> Bool
|
|
||||||
isSelfReference r = relTable r == relFTable r
|
|
||||||
|
|
||||||
data PayloadJSON =
|
|
||||||
-- | Cached attributes of a JSON payload
|
|
||||||
ProcessedJSON {
|
|
||||||
-- | This is the raw ByteString that comes from the request body.
|
|
||||||
-- We cache this instead of an Aeson Value because it was detected that for large payloads the encoding
|
|
||||||
-- had high memory usage, see https://github.com/PostgREST/postgrest/pull/1005 for more details
|
|
||||||
pjRaw :: BL.ByteString
|
|
||||||
-- | Keys of the object or if it's an array these keys are guaranteed to be the same across all its objects
|
|
||||||
, pjKeys :: S.Set Text
|
|
||||||
}|
|
|
||||||
RawJSON {
|
|
||||||
pjRaw :: BL.ByteString
|
|
||||||
}
|
|
||||||
|
|
||||||
data PJType = PJArray { pjaLength :: Int } | PJObject
|
|
||||||
|
|
||||||
data Proxy = Proxy {
|
|
||||||
proxyScheme :: Text
|
|
||||||
, proxyHost :: Text
|
|
||||||
, proxyPort :: Integer
|
|
||||||
, proxyPath :: Text
|
|
||||||
}
|
|
||||||
|
|
||||||
type Operator = Text
|
|
||||||
operators :: M.HashMap Operator SqlFragment
|
|
||||||
operators = M.union (M.fromList [
|
|
||||||
("eq", "="),
|
|
||||||
("gte", ">="),
|
|
||||||
("gt", ">"),
|
|
||||||
("lte", "<="),
|
|
||||||
("lt", "<"),
|
|
||||||
("neq", "<>"),
|
|
||||||
("like", "LIKE"),
|
|
||||||
("ilike", "ILIKE"),
|
|
||||||
("in", "IN"),
|
|
||||||
("is", "IS"),
|
|
||||||
("cs", "@>"),
|
|
||||||
("cd", "<@"),
|
|
||||||
("ov", "&&"),
|
|
||||||
("sl", "<<"),
|
|
||||||
("sr", ">>"),
|
|
||||||
("nxr", "&<"),
|
|
||||||
("nxl", "&>"),
|
|
||||||
("adj", "-|-")]) ftsOperators
|
|
||||||
|
|
||||||
ftsOperators :: M.HashMap Operator SqlFragment
|
|
||||||
ftsOperators = M.fromList [
|
|
||||||
("fts", "@@ to_tsquery"),
|
|
||||||
("plfts", "@@ plainto_tsquery"),
|
|
||||||
("phfts", "@@ phraseto_tsquery"),
|
|
||||||
("wfts", "@@ websearch_to_tsquery")
|
|
||||||
]
|
|
||||||
|
|
||||||
data OpExpr = OpExpr Bool Operation deriving (Eq)
|
|
||||||
data Operation = Op Operator SingleVal |
|
|
||||||
In ListVal |
|
|
||||||
Fts Operator (Maybe Language) SingleVal deriving (Eq)
|
|
||||||
type Language = Text
|
|
||||||
|
|
||||||
-- | Represents a single value in a filter, e.g. id=eq.singleval
|
|
||||||
type SingleVal = Text
|
|
||||||
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
|
|
||||||
type ListVal = [Text]
|
|
||||||
|
|
||||||
data LogicOperator = And | Or deriving Eq
|
|
||||||
instance Show LogicOperator where
|
|
||||||
show And = "AND"
|
|
||||||
show Or = "OR"
|
|
||||||
{-|
|
|
||||||
Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
|
|
||||||
|
|
||||||
And
|
|
||||||
/ \
|
|
||||||
name.eq.N Or
|
|
||||||
/ \
|
|
||||||
id.eq.1 id.eq.2
|
|
||||||
-}
|
|
||||||
data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Eq)
|
|
||||||
|
|
||||||
type FieldName = Text
|
|
||||||
{-|
|
|
||||||
Json path operations as specified in https://www.postgresql.org/docs/current/static/functions-json.html
|
|
||||||
-}
|
|
||||||
type JsonPath = [JsonOperation]
|
|
||||||
-- | Represents the single arrow `->` or double arrow `->>` operators
|
|
||||||
data JsonOperation = JArrow{jOp :: JsonOperand} | J2Arrow{jOp :: JsonOperand} deriving (Eq)
|
|
||||||
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text because we reuse our escaping functons and let pg do the casting with '1'::int
|
|
||||||
data JsonOperand = JKey{jVal :: Text} | JIdx{jVal :: Text} deriving (Eq)
|
|
||||||
|
|
||||||
type Field = (FieldName, JsonPath)
|
|
||||||
type Alias = Text
|
|
||||||
type Cast = Text
|
|
||||||
type NodeName = Text
|
|
||||||
|
|
||||||
|
|
||||||
{-|
|
|
||||||
Custom guc header, it's obtained by parsing the json in a:
|
|
||||||
`SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]'
|
|
||||||
-}
|
|
||||||
newtype GucHeader = GucHeader (CI.CI ByteString, ByteString)
|
|
||||||
|
|
||||||
instance JSON.FromJSON GucHeader where
|
|
||||||
parseJSON (JSON.Object o) = case headMay (M.toList o) of
|
|
||||||
Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (CI.mk $ toS k, toS s)
|
|
||||||
| otherwise -> mzero
|
|
||||||
_ -> mzero
|
|
||||||
parseJSON _ = mzero
|
|
||||||
|
|
||||||
unwrapGucHeader :: GucHeader -> Header
|
|
||||||
unwrapGucHeader (GucHeader (k, v)) = (k, v)
|
|
||||||
|
|
||||||
-- | Add headers not already included to allow the user to override them instead of duplicating them
|
|
||||||
addHeadersIfNotIncluded :: [Header] -> [Header] -> [Header]
|
|
||||||
addHeadersIfNotIncluded newHeaders initialHeaders =
|
|
||||||
filter (\(nk, _) -> isNothing $ find (\(ik, _) -> ik == nk) initialHeaders) newHeaders ++
|
|
||||||
initialHeaders
|
|
||||||
|
|
||||||
{-|
|
|
||||||
This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones.
|
|
||||||
Specifically, it will contain the name of the foreign key or the join table in many to many relations.
|
|
||||||
-}
|
|
||||||
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe EmbedHint)
|
|
||||||
-- | Disambiguates an embedding operation when there's multiple relationships between two tables.
|
|
||||||
-- | Can be the name of a foreign key constraint, column name or the junction in an m2m relationship.
|
|
||||||
type EmbedHint = Text
|
|
||||||
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"]
|
|
||||||
type EmbedPath = [Text]
|
|
||||||
data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Eq)
|
|
||||||
data JoinCondition = JoinCondition (QualifiedIdentifier, FieldName)
|
|
||||||
(QualifiedIdentifier, FieldName) deriving (Eq)
|
|
||||||
|
|
||||||
data ReadQuery = Select {
|
|
||||||
select :: [SelectItem]
|
|
||||||
, from :: QualifiedIdentifier
|
|
||||||
-- | A table alias is used in case of self joins
|
|
||||||
, fromAlias :: Maybe Alias
|
|
||||||
-- | Only used for Many to Many joins. Parent and Child joins use explicit joins.
|
|
||||||
, implicitJoins :: [QualifiedIdentifier]
|
|
||||||
, where_ :: [LogicTree]
|
|
||||||
, joinConditions :: [JoinCondition]
|
|
||||||
, order :: [OrderTerm]
|
|
||||||
, range_ :: NonnegRange
|
|
||||||
} deriving (Eq)
|
|
||||||
|
|
||||||
data MutateQuery =
|
|
||||||
Insert {
|
|
||||||
in_ :: QualifiedIdentifier
|
|
||||||
, insCols :: S.Set FieldName
|
|
||||||
, insBody :: Maybe BL.ByteString
|
|
||||||
, onConflict :: Maybe (PreferResolution, [FieldName])
|
|
||||||
, where_ :: [LogicTree]
|
|
||||||
, returning :: [FieldName]
|
|
||||||
}|
|
|
||||||
Update {
|
|
||||||
in_ :: QualifiedIdentifier
|
|
||||||
, updCols :: S.Set FieldName
|
|
||||||
, updBody :: Maybe BL.ByteString
|
|
||||||
, where_ :: [LogicTree]
|
|
||||||
, returning :: [FieldName]
|
|
||||||
}|
|
|
||||||
Delete {
|
|
||||||
in_ :: QualifiedIdentifier
|
|
||||||
, where_ :: [LogicTree]
|
|
||||||
, returning :: [FieldName]
|
|
||||||
}
|
|
||||||
|
|
||||||
type ReadRequest = Tree ReadNode
|
|
||||||
type MutateRequest = MutateQuery
|
|
||||||
|
|
||||||
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe EmbedHint, Depth))
|
|
||||||
type Depth = Integer
|
|
||||||
|
|
||||||
-- First level FieldNames(e.g get a,b from /table?select=a,b,other(c,d))
|
|
||||||
fstFieldNames :: ReadRequest -> [FieldName]
|
|
||||||
fstFieldNames (Node (sel, _) _) =
|
|
||||||
fst . view _1 <$> select sel
|
|
||||||
|
|
||||||
data PgVersion = PgVersion {
|
|
||||||
pgvNum :: Int32
|
|
||||||
, pgvName :: Text
|
|
||||||
} deriving (Eq, Generic, JSON.ToJSON)
|
|
||||||
|
|
||||||
instance Ord PgVersion where
|
|
||||||
(PgVersion v1 _) `compare` (PgVersion v2 _) = v1 `compare` v2
|
|
||||||
|
|
||||||
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
|
|
||||||
minimumPgVersion :: PgVersion
|
|
||||||
minimumPgVersion = pgVersion95
|
|
||||||
|
|
||||||
pgVersion95 :: PgVersion
|
|
||||||
pgVersion95 = PgVersion 90500 "9.5"
|
|
||||||
|
|
||||||
pgVersion96 :: PgVersion
|
|
||||||
pgVersion96 = PgVersion 90600 "9.6"
|
|
||||||
|
|
||||||
pgVersion100 :: PgVersion
|
|
||||||
pgVersion100 = PgVersion 100000 "10"
|
|
||||||
|
|
||||||
pgVersion109 :: PgVersion
|
|
||||||
pgVersion109 = PgVersion 100009 "10.9"
|
|
||||||
|
|
||||||
pgVersion110 :: PgVersion
|
|
||||||
pgVersion110 = PgVersion 110000 "11.0"
|
|
||||||
|
|
||||||
pgVersion112 :: PgVersion
|
|
||||||
pgVersion112 = PgVersion 110002 "11.2"
|
|
||||||
|
|
||||||
pgVersion114 :: PgVersion
|
|
||||||
pgVersion114 = PgVersion 110004 "11.4"
|
|
||||||
|
|
||||||
pgVersion121 :: PgVersion
|
|
||||||
pgVersion121 = PgVersion 120001 "12.1"
|
|
||||||
|
|
||||||
pgVersion130 :: PgVersion
|
|
||||||
pgVersion130 = PgVersion 130000 "13.0"
|
|
||||||
|
|
||||||
sourceCTEName :: SqlFragment
|
|
||||||
sourceCTEName = "pgrst_source"
|
|
||||||
|
|
||||||
-- | full jspath, e.g. .property[0].attr.detail
|
|
||||||
type JSPath = [JSPathExp]
|
|
||||||
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
|
|
||||||
data JSPathExp = JSPKey Text | JSPIdx Int
|
|
||||||
|
|
||||||
instance Show JSPathExp where
|
|
||||||
-- TODO: this needs to be quoted properly for special chars
|
|
||||||
show (JSPKey k) = "." <> show k
|
|
||||||
show (JSPIdx i) = "[" <> show i <> "]"
|
|
||||||
|
|
||||||
-- | Current database connection status data ConnectionStatus
|
|
||||||
data ConnectionStatus
|
|
||||||
= NotConnected
|
|
||||||
| Connected PgVersion
|
|
||||||
| FatalConnectionError Text
|
|
||||||
deriving (Eq)
|
|
||||||
|
|
||||||
-- | Schema cache status
|
|
||||||
data SCacheStatus
|
|
||||||
= SCLoaded
|
|
||||||
| SCOnRetry
|
|
||||||
| SCFatalFail
|
|
||||||
|
|
||||||
data LogLevel = LogCrit | LogError | LogWarn | LogInfo
|
|
||||||
|
|
||||||
instance Show LogLevel where
|
|
||||||
show LogCrit = "crit"
|
|
||||||
show LogError = "error"
|
|
||||||
show LogWarn = "warn"
|
|
||||||
show LogInfo = "info"
|
|
||||||
@@ -7,7 +7,8 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import PostgREST.Types (PgVersion, pgVersion112)
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion112)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
|||||||
@@ -7,7 +7,8 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import PostgREST.Types (PgVersion, pgVersion112)
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion112)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
|||||||
@@ -11,7 +11,9 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
|
||||||
import PostgREST.Types (PgVersion, pgVersion112, pgVersion130)
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion112,
|
||||||
|
pgVersion130)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
|||||||
@@ -7,8 +7,9 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import PostgREST.Types (PgVersion, pgVersion112, pgVersion121,
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion112,
|
||||||
pgVersion95)
|
pgVersion121, pgVersion95)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import Test.Hspec.Wai.JSON
|
|||||||
import Protolude
|
import Protolude
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import PostgREST.Types (PgVersion, pgVersion96)
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion96)
|
||||||
|
|
||||||
spec :: PgVersion -> SpecWith ((), Application)
|
spec :: PgVersion -> SpecWith ((), Application)
|
||||||
spec actualPgVersion =
|
spec actualPgVersion =
|
||||||
|
|||||||
@@ -8,8 +8,8 @@ import Test.Hspec hiding (pendingWith)
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import PostgREST.Types (PgVersion, pgVersion112, pgVersion121,
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion112,
|
||||||
pgVersion96)
|
pgVersion121, pgVersion96)
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
|||||||
@@ -11,9 +11,11 @@ import Test.Hspec.Wai
|
|||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
|
||||||
import PostgREST.Types (PgVersion, pgVersion100, pgVersion109,
|
import PostgREST.DbStructure.PgVersion (PgVersion, pgVersion100,
|
||||||
pgVersion110, pgVersion112, pgVersion114,
|
pgVersion109, pgVersion110,
|
||||||
|
pgVersion112, pgVersion114,
|
||||||
pgVersion96)
|
pgVersion96)
|
||||||
|
|
||||||
import Protolude hiding (get)
|
import Protolude hiding (get)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -13,9 +13,9 @@ import Data.IORef
|
|||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|
||||||
import PostgREST.App (postgrest)
|
import PostgREST.App (postgrest)
|
||||||
import PostgREST.Config (AppConfig (..))
|
import PostgREST.Config (AppConfig (..), LogLevel (..))
|
||||||
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
import PostgREST.DbStructure (getDbStructure, getPgVersion)
|
||||||
import PostgREST.Types (LogLevel (..), pgVersion96)
|
import PostgREST.DbStructure.PgVersion (pgVersion96)
|
||||||
import Protolude hiding (toList, toS)
|
import Protolude hiding (toList, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|||||||
+6
-2
@@ -14,8 +14,12 @@ import Text.Heredoc
|
|||||||
import Protolude hiding (get, toS)
|
import Protolude hiding (get, toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
import PostgREST.QueryBuilder (requestToCallProcQuery)
|
import PostgREST.Query.QueryBuilder (requestToCallProcQuery)
|
||||||
import PostgREST.Types
|
import PostgREST.Request.ApiRequest (PayloadJSON (..))
|
||||||
|
|
||||||
|
import PostgREST.DbStructure.Identifiers
|
||||||
|
import PostgREST.DbStructure.Proc
|
||||||
|
import PostgREST.Request.Preferences
|
||||||
|
|
||||||
import SpecHelper (getEnvVarWithDefault)
|
import SpecHelper (getEnvVarWithDefault)
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -22,8 +22,8 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Text.Heredoc
|
import Text.Heredoc
|
||||||
|
|
||||||
import PostgREST.Config (AppConfig (..), parseSecret)
|
import PostgREST.Config (AppConfig (..), JSPathExp (..),
|
||||||
import PostgREST.Types (JSPathExp (..), LogLevel (..))
|
LogLevel (..), parseSecret)
|
||||||
import Protolude hiding (toS)
|
import Protolude hiding (toS)
|
||||||
import Protolude.Conv (toS)
|
import Protolude.Conv (toS)
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user