Allow multiple schemas to be exposed in one instance (#1450)
The schema to use can be selected through the headers `Accept-Profile` for GET/HEAD and `Content-Profile` for POST/PATCH/PUT/DELETE. This is based on the https://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/ spec. Also increase all memory tests by 1M(otherwise CI fails). Co-authored-by: Mahmoud Kassem <MKassem@gk-software.com> Co-authored-by: Mahmoud Kassem <mahmoud_k@mail.com>
This commit is contained in:
co-authored by
Mahmoud Kassem
Mahmoud Kassem
parent
a80eb2ff0e
commit
691bb5640d
@@ -28,7 +28,8 @@ import qualified Data.Vector as V
|
||||
|
||||
import Control.Arrow ((***))
|
||||
import Data.Aeson.Types (emptyArray, emptyObject)
|
||||
import Data.List (last, lookup, partition)
|
||||
import Data.List (elem, last, lookup, partition)
|
||||
import Data.List.NonEmpty (NonEmpty, head)
|
||||
import Data.Maybe (fromJust)
|
||||
import Data.Ranged.Ranges (Range (..), emptyRange,
|
||||
rangeIntersection)
|
||||
@@ -48,7 +49,7 @@ import PostgREST.RangeQuery (NonnegRange, allRange, rangeGeq,
|
||||
rangeLimit, rangeOffset, rangeRequested,
|
||||
restrictRange)
|
||||
import PostgREST.Types
|
||||
import Protolude
|
||||
import Protolude hiding (head)
|
||||
|
||||
type RequestBody = BL.ByteString
|
||||
|
||||
@@ -96,11 +97,14 @@ data ApiRequest = ApiRequest {
|
||||
, iCookies :: [(Text, Text)] -- ^ Request Cookies
|
||||
, iPath :: ByteString -- ^ Raw request path
|
||||
, iMethod :: ByteString -- ^ Raw request method
|
||||
, iProfile :: Maybe Schema -- ^ The request profile for enabling use of multiple schemas. Follows the spec in hhttps://www.w3.org/TR/dx-prof-conneg/ttps://www.w3.org/TR/dx-prof-conneg/.
|
||||
, iSchema :: Schema -- ^ The request schema. Can vary depending on iProfile.
|
||||
}
|
||||
|
||||
-- | Examines HTTP request and translates it into user intent.
|
||||
userApiRequest :: Schema -> Maybe Text -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||
userApiRequest schema rootSpec req reqBody
|
||||
userApiRequest :: NonEmpty Schema -> Maybe Text -> Request -> RequestBody -> Either ApiRequestError ApiRequest
|
||||
userApiRequest confSchemas rootSpec req reqBody
|
||||
| isJust profile && fromJust profile `notElem` confSchemas = Left $ UnacceptableSchema $ toList confSchemas
|
||||
| isTargetingProc && method `notElem` ["HEAD", "GET", "POST"] = Left ActionInappropriate
|
||||
| topLevelRange == emptyRange = Left InvalidRange
|
||||
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
|
||||
@@ -137,6 +141,8 @@ userApiRequest schema rootSpec req reqBody
|
||||
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
|
||||
, iPath = rawPathInfo req
|
||||
, iMethod = method
|
||||
, iProfile = profile
|
||||
, iSchema = schema
|
||||
}
|
||||
where
|
||||
-- queryString with '+' converted to ' '(space)
|
||||
@@ -208,6 +214,18 @@ userApiRequest schema rootSpec req reqBody
|
||||
"DELETE" -> ActionDelete
|
||||
"OPTIONS" -> ActionInfo
|
||||
_ -> ActionInspect{isHead=False}
|
||||
|
||||
defaultSchema = head confSchemas
|
||||
profile
|
||||
| length confSchemas <= 1 -- only enable content negotiation by profile when there are multiple schemas specified in the config
|
||||
= Nothing
|
||||
| action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionDelete] -- POST/PATCH/PUT/DELETE don't use the same header as per the spec
|
||||
= Just $ maybe defaultSchema toS $ lookupHeader "Content-Profile"
|
||||
| action `elem` [ActionRead True, ActionRead False, ActionInvoke InvGet, ActionInvoke InvHead, ActionInvoke InvPost,
|
||||
ActionInspect False, ActionInspect True, ActionInfo]
|
||||
= Just $ maybe defaultSchema toS $ lookupHeader "Accept-Profile"
|
||||
| otherwise = Nothing
|
||||
schema = fromMaybe defaultSchema profile
|
||||
target = case path of
|
||||
[] -> case rootSpec of
|
||||
Just pName -> TargetProc (QualifiedIdentifier schema pName) True
|
||||
|
||||
+29
-21
@@ -78,8 +78,9 @@ postgrest conf refDbStructure pool getTime worker =
|
||||
Nothing -> respond . errorResponseFor $ ConnectionLostError
|
||||
Just dbStructure -> do
|
||||
response <- do
|
||||
-- Need to parse ?columns early because findProc needs it to solve overloaded functions
|
||||
let apiReq = userApiRequest (configSchema conf) (configRootSpec conf) req body
|
||||
-- Need to parse ?columns early because findProc needs it to solve overloaded functions.
|
||||
-- TODO: move this logic to the app function
|
||||
let apiReq = userApiRequest (configSchemas conf) (configRootSpec conf) req body
|
||||
apiReqCols = (,) <$> apiReq <*> (pRequestColumns =<< iColumns <$> apiReq)
|
||||
case apiReqCols of
|
||||
Left err -> return . errorResponseFor $ err
|
||||
@@ -145,8 +146,9 @@ app dbStructure proc cols conf apiRequest =
|
||||
else pure tableTotal
|
||||
| otherwise -> pure tableTotal
|
||||
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal total
|
||||
headers = addHeadersIfNotIncluded
|
||||
[toHeader contentType, contentRange, contentLocationH tName (iCanonicalQS apiRequest)]
|
||||
headers = addHeadersIfNotIncluded (catMaybes [
|
||||
Just $ toHeader contentType, Just contentRange,
|
||||
Just $ contentLocationH tName (iCanonicalQS apiRequest), profileH])
|
||||
(unwrapGucHeader <$> ghdrs)
|
||||
rBody = if headersOnly then mempty else toS body
|
||||
return $
|
||||
@@ -168,19 +170,18 @@ app dbStructure proc cols conf apiRequest =
|
||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
||||
Right ghdrs -> do
|
||||
let
|
||||
(ctHeader, rBody) = if iPreferRepresentation apiRequest == Full
|
||||
then (Just $ toHeader contentType, toS body)
|
||||
else (Nothing, mempty)
|
||||
headers = addHeadersIfNotIncluded (catMaybes [
|
||||
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
|
||||
then ([Just $ toHeader contentType, profileH], toS body)
|
||||
else ([], mempty)
|
||||
headers = addHeadersIfNotIncluded (catMaybes ([
|
||||
if null fields
|
||||
then Nothing
|
||||
else Just $ locationH tName fields
|
||||
, ctHeader
|
||||
, Just $ contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
|
||||
, if null pkCols && isNothing (iOnConflict apiRequest)
|
||||
then Nothing
|
||||
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
|
||||
]) (unwrapGucHeader <$> ghdrs)
|
||||
] ++ ctHeaders)) (unwrapGucHeader <$> ghdrs)
|
||||
if contentType == CTSingularJSON && queryTotal /= 1
|
||||
then do
|
||||
HT.condemn
|
||||
@@ -206,10 +207,10 @@ app dbStructure proc cols conf apiRequest =
|
||||
| iPreferRepresentation apiRequest == Full = status200
|
||||
| otherwise = status204
|
||||
contentRangeHeader = contentRangeH 0 (queryTotal - 1) $ if shouldCount then Just queryTotal else Nothing
|
||||
(ctHeader, rBody) = if iPreferRepresentation apiRequest == Full
|
||||
then (Just $ toHeader contentType, toS body)
|
||||
else (Nothing, mempty)
|
||||
headers = addHeadersIfNotIncluded (catMaybes [Just contentRangeHeader, ctHeader]) (unwrapGucHeader <$> ghdrs)
|
||||
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
|
||||
then ([Just $ toHeader contentType, profileH], toS body)
|
||||
else ([], mempty)
|
||||
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
|
||||
if contentType == CTSingularJSON && queryTotal /= 1
|
||||
then do
|
||||
HT.condemn
|
||||
@@ -239,7 +240,7 @@ app dbStructure proc cols conf apiRequest =
|
||||
case gucHeaders of
|
||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
||||
Right ghdrs -> do
|
||||
let headers = addHeadersIfNotIncluded [toHeader contentType] (unwrapGucHeader <$> ghdrs)
|
||||
let headers = addHeadersIfNotIncluded (catMaybes [Just $ toHeader contentType, profileH]) (unwrapGucHeader <$> ghdrs)
|
||||
(status, rBody) = if iPreferRepresentation apiRequest == Full then (status200, toS body) else (status204, mempty)
|
||||
-- Makes sure the querystring pk matches the payload pk
|
||||
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected
|
||||
@@ -267,10 +268,10 @@ app dbStructure proc cols conf apiRequest =
|
||||
let
|
||||
status = if iPreferRepresentation apiRequest == Full then status200 else status204
|
||||
contentRangeHeader = contentRangeH 1 0 $ if shouldCount then Just queryTotal else Nothing
|
||||
(ctHeader, rBody) = if iPreferRepresentation apiRequest == Full
|
||||
then (Just $ toHeader contentType, toS body)
|
||||
else (Nothing, mempty)
|
||||
headers = addHeadersIfNotIncluded (catMaybes [Just contentRangeHeader, ctHeader]) (unwrapGucHeader <$> ghdrs)
|
||||
(ctHeaders, rBody) = if iPreferRepresentation apiRequest == Full
|
||||
then ([Just $ toHeader contentType, profileH], toS body)
|
||||
else ([], mempty)
|
||||
headers = addHeadersIfNotIncluded (catMaybes ctHeaders ++ [contentRangeHeader]) (unwrapGucHeader <$> ghdrs)
|
||||
if contentType == CTSingularJSON
|
||||
&& queryTotal /= 1
|
||||
then do
|
||||
@@ -305,7 +306,9 @@ app dbStructure proc cols conf apiRequest =
|
||||
Left _ -> return . errorResponseFor $ GucHeadersError
|
||||
Right ghdrs -> do
|
||||
let (status, contentRange) = rangeStatusHeader topLevelRange queryTotal tableTotal
|
||||
headers = addHeadersIfNotIncluded [toHeader contentType, contentRange] (unwrapGucHeader <$> ghdrs)
|
||||
headers = addHeadersIfNotIncluded
|
||||
(catMaybes [Just $ toHeader contentType, Just contentRange, profileH])
|
||||
(unwrapGucHeader <$> ghdrs)
|
||||
rBody = if invMethod == InvHead then mempty else toS body
|
||||
if contentType == CTSingularJSON && queryTotal /= 1
|
||||
then do
|
||||
@@ -329,7 +332,7 @@ app dbStructure proc cols conf apiRequest =
|
||||
H.statement tSchema accessibleTables <*>
|
||||
H.statement tSchema schemaDescription <*>
|
||||
H.statement tSchema accessibleProcs
|
||||
return $ responseLBS status200 [toHeader CTOpenAPI] (if headersOnly then mempty else toS body)
|
||||
return $ responseLBS status200 (catMaybes [Just $ toHeader CTOpenAPI, profileH]) (if headersOnly then mempty else toS body)
|
||||
|
||||
_ -> return notFound
|
||||
|
||||
@@ -343,6 +346,7 @@ app dbStructure proc cols conf apiRequest =
|
||||
topLevelRange = iTopLevelRange apiRequest
|
||||
returnsScalar = maybe False procReturnsScalar proc
|
||||
pgVer = pgVersion dbStructure
|
||||
profileH = contentProfileH <$> iProfile apiRequest
|
||||
|
||||
readSqlParts s t =
|
||||
let
|
||||
@@ -413,3 +417,7 @@ locationH tName fields =
|
||||
contentLocationH :: TableName -> ByteString -> Header
|
||||
contentLocationH tName qString =
|
||||
("Content-Location", "/" <> toS tName <> if BS.null qString then mempty else "?" <> toS qString)
|
||||
|
||||
contentProfileH :: Schema -> Header
|
||||
contentProfileH schema =
|
||||
("Content-Profile", toS schema)
|
||||
|
||||
@@ -37,6 +37,7 @@ import Control.Lens (preview)
|
||||
import Control.Monad (fail)
|
||||
import Crypto.JWT (StringOrURI, stringOrUri)
|
||||
import Data.List (lookup)
|
||||
import Data.List.NonEmpty (NonEmpty, fromList)
|
||||
import Data.Scientific (floatingOrInteger)
|
||||
import Data.Text (dropEnd, dropWhileEnd,
|
||||
intercalate, lines, splitOn,
|
||||
@@ -71,7 +72,7 @@ data AppConfig = AppConfig {
|
||||
configDatabase :: Text
|
||||
, configAnonRole :: Text
|
||||
, configOpenAPIProxyUri :: Maybe Text
|
||||
, configSchema :: Text
|
||||
, configSchemas :: NonEmpty Text
|
||||
, configHost :: Text
|
||||
, configPort :: Int
|
||||
, configSocket :: Maybe Text
|
||||
@@ -154,8 +155,8 @@ readOptions = do
|
||||
AppConfig
|
||||
<$> reqString "db-uri"
|
||||
<*> reqString "db-anon-role"
|
||||
<*> optString "openapi-server-proxy-uri"
|
||||
<*> reqString "db-schema"
|
||||
<*> optString "server-proxy-uri"
|
||||
<*> (fromList . splitOnCommas <$> reqValue "db-schema")
|
||||
<*> (fromMaybe "!4" <$> optString "server-host")
|
||||
<*> (fromMaybe 3000 <$> optInt "server-port")
|
||||
<*> optString "server-unix-socket"
|
||||
@@ -199,6 +200,9 @@ readOptions = do
|
||||
reqString :: C.Key -> C.Parser C.Config Text
|
||||
reqString k = C.required k C.string
|
||||
|
||||
reqValue :: C.Key -> C.Parser C.Config C.Value
|
||||
reqValue k = C.required k C.value
|
||||
|
||||
optString :: C.Key -> C.Parser C.Config (Maybe Text)
|
||||
optString k = mfilter (/= "") <$> C.optional k C.string
|
||||
|
||||
|
||||
@@ -44,15 +44,15 @@ import PostgREST.Private.Common
|
||||
import PostgREST.Types
|
||||
import Protolude
|
||||
|
||||
getDbStructure :: Schema -> PgVersion -> HT.Transaction DbStructure
|
||||
getDbStructure schema pgVer = do
|
||||
HT.sql "set local schema ''" -- for getting the fully qualified name(schema.name) of every db object
|
||||
getDbStructure :: [Schema] -> PgVersion -> HT.Transaction DbStructure
|
||||
getDbStructure schemas pgVer = do
|
||||
HT.sql "set local schema ''" -- This voids the search path. The following queries need this for getting the fully qualified name(schema.name) of every db object
|
||||
tabs <- HT.statement () allTables
|
||||
cols <- HT.statement schema $ allColumns tabs
|
||||
srcCols <- HT.statement schema $ allSourceColumns cols pgVer
|
||||
cols <- HT.statement schemas $ allColumns tabs
|
||||
srcCols <- HT.statement schemas $ allSourceColumns cols pgVer
|
||||
m2oRels <- HT.statement () $ allM2ORels tabs cols
|
||||
keys <- HT.statement () $ allPrimaryKeys tabs
|
||||
procs <- HT.statement schema allProcs
|
||||
procs <- HT.statement schemas allProcs
|
||||
|
||||
let rels = addM2MRels . addO2MRels $ addViewM2ORels srcCols m2oRels
|
||||
cols' = addForeignKeys rels cols
|
||||
@@ -126,13 +126,14 @@ sourceColumnFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
|
||||
col2 = findCol s2 t2 c2
|
||||
findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
|
||||
|
||||
decodeProcs :: HD.Result (M.HashMap Text [ProcDescription])
|
||||
decodeProcs :: HD.Result ProcsMap
|
||||
decodeProcs =
|
||||
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
|
||||
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowList tblRow
|
||||
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addKey) <$> HD.rowList procRow
|
||||
where
|
||||
tblRow = ProcDescription
|
||||
procRow = ProcDescription
|
||||
<$> column HD.text
|
||||
<*> column HD.text
|
||||
<*> nullableColumn HD.text
|
||||
<*> (parseArgs <$> column HD.text)
|
||||
<*> (parseRetType
|
||||
@@ -142,8 +143,8 @@ decodeProcs =
|
||||
<*> column HD.char)
|
||||
<*> (parseVolatility <$> column HD.char)
|
||||
|
||||
addName :: ProcDescription -> (Text, ProcDescription)
|
||||
addName pd = (pdName pd, pd)
|
||||
addKey :: ProcDescription -> (QualifiedIdentifier, ProcDescription)
|
||||
addKey pd = (QualifiedIdentifier (pdSchema pd) (pdName pd), pd)
|
||||
|
||||
parseArgs :: Text -> [PgArg]
|
||||
parseArgs = mapMaybe parseArg . filter (not . isPrefixOf "OUT" . toS) . map strip . split (==',')
|
||||
@@ -176,31 +177,34 @@ decodeProcs =
|
||||
| v == 's' = Stable
|
||||
| otherwise = Volatile -- only 'v' can happen here
|
||||
|
||||
allProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
|
||||
allProcs = H.Statement (toS procsSqlQuery) (param HE.text) decodeProcs True
|
||||
allProcs :: H.Statement [Schema] ProcsMap
|
||||
allProcs = H.Statement (toS sql) (arrayParam HE.text) decodeProcs True
|
||||
where
|
||||
sql = procsSqlQuery <> " WHERE pn.nspname = ANY($1)"
|
||||
|
||||
accessibleProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
|
||||
accessibleProcs :: H.Statement Schema ProcsMap
|
||||
accessibleProcs = H.Statement (toS sql) (param HE.text) decodeProcs True
|
||||
where
|
||||
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
|
||||
sql = procsSqlQuery <> " WHERE pn.nspname = $1 AND has_function_privilege(p.oid, 'execute')"
|
||||
|
||||
procsSqlQuery :: SqlQuery
|
||||
procsSqlQuery = [q|
|
||||
SELECT p.proname as "proc_name",
|
||||
d.description as "proc_description",
|
||||
pg_get_function_arguments(p.oid) as "args",
|
||||
tn.nspname as "rettype_schema",
|
||||
coalesce(comp.relname, t.typname) as "rettype_name",
|
||||
p.proretset as "rettype_is_setof",
|
||||
t.typtype as "rettype_typ",
|
||||
p.provolatile
|
||||
SELECT
|
||||
pn.nspname as "proc_schema",
|
||||
p.proname as "proc_name",
|
||||
d.description as "proc_description",
|
||||
pg_get_function_arguments(p.oid) as "args",
|
||||
tn.nspname as "rettype_schema",
|
||||
coalesce(comp.relname, t.typname) as "rettype_name",
|
||||
p.proretset as "rettype_is_setof",
|
||||
t.typtype as "rettype_typ",
|
||||
p.provolatile
|
||||
FROM pg_proc p
|
||||
JOIN pg_namespace pn ON pn.oid = p.pronamespace
|
||||
JOIN pg_type t ON t.oid = p.prorettype
|
||||
JOIN pg_namespace tn ON tn.oid = t.typnamespace
|
||||
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
|
||||
LEFT JOIN pg_catalog.pg_description as d on d.objoid = p.oid
|
||||
WHERE pn.nspname = $1
|
||||
|]
|
||||
|
||||
schemaDescription :: H.Statement Schema (Maybe Text)
|
||||
@@ -384,9 +388,9 @@ allTables =
|
||||
GROUP BY table_schema, table_name, insertable
|
||||
ORDER BY table_schema, table_name |]
|
||||
|
||||
allColumns :: [Table] -> H.Statement Schema [Column]
|
||||
allColumns :: [Table] -> H.Statement [Schema] [Column]
|
||||
allColumns tabs =
|
||||
H.Statement sql (param HE.text) (decodeColumns tabs) True
|
||||
H.Statement sql (arrayParam HE.text) (decodeColumns tabs) True
|
||||
where
|
||||
sql = [q|
|
||||
SELECT DISTINCT
|
||||
@@ -424,7 +428,7 @@ allColumns tabs =
|
||||
AND c.relkind IN ('r', 'v', 'f', 'm')
|
||||
AND r.conrelid = c.oid
|
||||
AND c.relnamespace = n.oid
|
||||
AND n.nspname NOT IN ('pg_catalog', 'information_schema', $1)
|
||||
AND n.nspname <> ANY (ARRAY['pg_catalog', 'information_schema'] || $1)
|
||||
),
|
||||
/*
|
||||
-- CTE based on information_schema.columns
|
||||
@@ -526,7 +530,7 @@ allColumns tabs =
|
||||
AND a.attnum > 0
|
||||
AND NOT a.attisdropped
|
||||
AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char", 'm'::"char"]))
|
||||
AND (nc.nspname = $1 OR kc.r_oid IS NOT NULL) /*--filter only columns that are FK/PK or in the api schema */
|
||||
AND (nc.nspname = ANY ($1) OR kc.r_oid IS NOT NULL) /*--filter only columns that are FK/PK or in the api schema */
|
||||
/*--AND (pg_has_role(c.relowner, 'USAGE'::text) OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
|
||||
)
|
||||
SELECT
|
||||
@@ -719,9 +723,9 @@ pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey
|
||||
pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
|
||||
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
|
||||
|
||||
allSourceColumns :: [Column] -> PgVersion -> H.Statement Schema [SourceColumn]
|
||||
allSourceColumns :: [Column] -> PgVersion -> H.Statement [Schema] [SourceColumn]
|
||||
allSourceColumns cols pgVer =
|
||||
H.Statement sql (param HE.text) (decodeSourceColumns cols) True
|
||||
H.Statement sql (arrayParam HE.text) (decodeSourceColumns cols) True
|
||||
-- query explanation at https://gist.github.com/steve-chavez/7ee0e6590cddafb532e5f00c46275569
|
||||
where
|
||||
subselectRegex :: Text
|
||||
@@ -740,7 +744,7 @@ allSourceColumns cols pgVer =
|
||||
from pg_class c
|
||||
join pg_namespace n on n.oid = c.relnamespace
|
||||
join pg_rewrite r on r.ev_class = c.oid
|
||||
where (c.relkind in ('v', 'm')) and n.nspname = $1
|
||||
where (c.relkind in ('v', 'm')) and n.nspname = ANY ($1)
|
||||
),
|
||||
removed_subselects as(
|
||||
select
|
||||
|
||||
@@ -51,6 +51,7 @@ data ApiRequestError
|
||||
| NoRelBetween Text Text
|
||||
| AmbiguousRelBetween Text Text [Relation]
|
||||
| InvalidFilters
|
||||
| UnacceptableSchema [Text]
|
||||
| UnknownRelation -- Unreachable?
|
||||
| UnsupportedVerb -- Unreachable?
|
||||
deriving (Show, Eq)
|
||||
@@ -65,6 +66,7 @@ instance PgrstError ApiRequestError where
|
||||
status (ParseRequestError _ _) = HT.status400
|
||||
status (NoRelBetween _ _) = HT.status400
|
||||
status AmbiguousRelBetween{} = HT.status300
|
||||
status (UnacceptableSchema _) = HT.status406
|
||||
|
||||
headers _ = [toHeader CTApplicationJSON]
|
||||
|
||||
@@ -89,6 +91,8 @@ instance JSON.ToJSON ApiRequestError where
|
||||
"message" .= ("Unsupported HTTP verb" :: Text)]
|
||||
toJSON InvalidFilters = JSON.object [
|
||||
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)]
|
||||
toJSON (UnacceptableSchema schemas) = JSON.object [
|
||||
"message" .= ("The schema must be one of the following: " <> T.intercalate ", " schemas)]
|
||||
|
||||
compressedRel :: Relation -> JSON.Value
|
||||
compressedRel rel =
|
||||
|
||||
@@ -27,7 +27,7 @@ import PostgREST.Config (AppConfig (..), corsPolicy)
|
||||
import PostgREST.Error (SimpleError (JwtTokenInvalid, JwtTokenMissing),
|
||||
errorResponseFor)
|
||||
import PostgREST.QueryBuilder (setLocalQuery, setLocalSearchPathQuery)
|
||||
import Protolude
|
||||
import Protolude hiding (head)
|
||||
|
||||
runWithClaims :: AppConfig -> JWTAttempt ->
|
||||
(ApiRequest -> H.Transaction Response) ->
|
||||
@@ -50,7 +50,7 @@ runWithClaims conf eClaims app req =
|
||||
appSettingsSql = setLocalQuery mempty <$> configSettings conf
|
||||
setRoleSql = maybeToList $ (\x ->
|
||||
setLocalQuery mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
|
||||
setSearchPathSql = setLocalSearchPathQuery $ configSchema conf : configExtraSearchPath conf
|
||||
setSearchPathSql = setLocalSearchPathQuery (iSchema req : configExtraSearchPath conf)
|
||||
-- role claim defaults to anon if not specified in jwt
|
||||
claimsWithRole = M.union claims (M.singleton "role" anon)
|
||||
anon = JSON.String . toS $ configAnonRole conf
|
||||
|
||||
@@ -20,3 +20,6 @@ element = HD.element . HD.nonNullable
|
||||
|
||||
param :: HE.Value a -> HE.Params a
|
||||
param = HE.param . HE.nonNullable
|
||||
|
||||
arrayParam :: HE.Value a -> HE.Params [a]
|
||||
arrayParam = param . HE.array . HE.dimension foldl' . HE.element . HE.nonNullable
|
||||
|
||||
+20
-11
@@ -2,6 +2,7 @@
|
||||
Module : PostgREST.Types
|
||||
Description : PostgREST common types and functions used by the rest of the modules
|
||||
-}
|
||||
{-# LANGUAGE DeriveGeneric #-}
|
||||
{-# LANGUAGE DuplicateRecordFields #-}
|
||||
|
||||
module PostgREST.Types where
|
||||
@@ -105,8 +106,7 @@ data DbStructure = DbStructure {
|
||||
, dbColumns :: [Column]
|
||||
, dbRelations :: [Relation]
|
||||
, dbPrimaryKeys :: [PrimaryKey]
|
||||
-- ProcDescription is a list because a function can be overloaded
|
||||
, dbProcs :: M.HashMap Text [ProcDescription]
|
||||
, dbProcs :: ProcsMap
|
||||
, pgVersion :: PgVersion
|
||||
} deriving (Show, Eq)
|
||||
|
||||
@@ -132,7 +132,8 @@ data ProcVolatility = Volatile | Stable | Immutable
|
||||
deriving (Eq, Show, Ord)
|
||||
|
||||
data ProcDescription = ProcDescription {
|
||||
pdName :: Text
|
||||
pdSchema :: Schema
|
||||
, pdName :: Text
|
||||
, pdDescription :: Maybe Text
|
||||
, pdArgs :: [PgArg]
|
||||
, pdReturnType :: RetType
|
||||
@@ -141,18 +142,23 @@ data ProcDescription = ProcDescription {
|
||||
|
||||
-- Order by least number of args in the case of overloaded functions
|
||||
instance Ord ProcDescription where
|
||||
ProcDescription name1 des1 args1 rt1 vol1 `compare` ProcDescription name2 des2 args2 rt2 vol2
|
||||
| name1 == name2 && length args1 < length args2 = LT
|
||||
| name1 == name2 && length args1 > length args2 = GT
|
||||
| otherwise = (name1, des1, args1, rt1, vol1) `compare` (name2, des2, args2, rt2, vol2)
|
||||
ProcDescription schema1 name1 des1 args1 rt1 vol1 `compare` ProcDescription schema2 name2 des2 args2 rt2 vol2
|
||||
| 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) `compare` (schema2, name2, des2, args2, rt2, vol2)
|
||||
|
||||
-- | 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 -> M.HashMap Text [ProcDescription] -> Maybe ProcDescription
|
||||
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> ProcsMap -> Maybe ProcDescription
|
||||
findProc qi payloadKeys paramsAsSingleObject allProcs =
|
||||
case M.lookup (qiName qi) allProcs of
|
||||
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
|
||||
@@ -254,8 +260,8 @@ data OrderTerm = OrderTerm {
|
||||
data QualifiedIdentifier = QualifiedIdentifier {
|
||||
qiSchema :: Schema
|
||||
, qiName :: TableName
|
||||
} deriving (Show, Eq, Ord)
|
||||
|
||||
} deriving (Show, Eq, Ord, Generic)
|
||||
instance Hashable QualifiedIdentifier
|
||||
|
||||
-- | The relationship [cardinality](https://en.wikipedia.org/wiki/Cardinality_(data_modeling)).
|
||||
-- | TODO: missing one-to-one
|
||||
@@ -512,6 +518,9 @@ pgVersion112 = PgVersion 110002 "11.2"
|
||||
pgVersion114 :: PgVersion
|
||||
pgVersion114 = PgVersion 110004 "11.4"
|
||||
|
||||
pgVersion121 :: PgVersion
|
||||
pgVersion121 = PgVersion 120001 "12.1"
|
||||
|
||||
sourceCTEName :: SqlFragment
|
||||
sourceCTEName = "pg_source"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user