packages feed

postgrest 7.0.0 → 7.0.1

raw patch · 29 files changed

+545/−408 lines, 29 filesdep ~aesondep ~basedep ~base64-bytestring

Dependency ranges changed: aeson, base, base64-bytestring, hspec-wai, hspec-wai-json, lens, network-uri, optparse-applicative, protolude, swagger2, time, wai-extra, wai-middleware-static

Files

CHANGELOG.md view
@@ -9,6 +9,16 @@  ### Fixed +## [7.0.1] - 2020-05-18++### Fixed++- #1473, Fix overloaded computed columns on RPC - @wolfgangwalther+- #1471, Fix POST, PATCH, DELETE with ?select= and return=minimal and PATCH with empty body - @wolfgangwalther+- #1500, Fix missing `openapi-server-proxy-uri` config option - @steve-chavez+- #1508, Fix `Content-Profile` not working for POST RPC - @steve-chavez+- #1452, Fix PUT restriction for all columns - @steve-chavez+ ## [7.0.0] - 2020-04-03  ### Added
main/Main.hs view
@@ -17,8 +17,7 @@                                  readIORef) import Data.String              (IsString (..)) import Data.Text                (pack, replace, strip, stripPrefix)-import Data.Text.Encoding       (decodeUtf8, encodeUtf8)-import Data.Text.IO             (hPutStrLn, readFile)+import Data.Text.IO             (hPutStrLn) import Data.Time.Clock          (getCurrentTime) import Network.Wai.Handler.Warp (defaultSettings, runSettings,                                  setHost, setPort, setServerName)@@ -34,7 +33,8 @@ import PostgREST.Types       (ConnectionStatus (..), DbStructure,                               PgVersion (..), Schema,                               minimumPgVersion)-import Protolude             hiding (hPutStrLn, head, replace)+import Protolude             hiding (hPutStrLn, head, replace, toS)+import Protolude.Conv        (toS)   #ifndef mingw32_HOST_OS
postgrest.cabal view
@@ -1,5 +1,5 @@ name:               postgrest-version:            7.0.0+version:            7.0.1 synopsis:           REST API for any Postgres database description:        Reads the schema of a PostgreSQL database and creates RESTful routes                     for the tables and views, supporting all HTTP verbs that security@@ -19,10 +19,10 @@   type:     git   location: git://github.com/PostgREST/postgrest.git -flag ci+flag FailOnWarn   default:     False   manual:      True-  description: No warnings allowed in continuous integration+  description: No warnings allowed  library   exposed-modules:    PostgREST.ApiRequest@@ -43,12 +43,12 @@                       PostgREST.Private.Common                       PostgREST.Private.QueryFragment   hs-source-dirs:     src-  build-depends:      base                      >= 4.9 && < 4.14+  build-depends:      base                      >= 4.9 && < 4.15                     , HTTP                      >= 4000.3.7 && < 4000.4                     , Ranged-sets               >= 0.3 && < 0.5-                    , aeson                     >= 0.11.3 && < 1.5+                    , aeson                     >= 1.4.7 && < 1.5                     , ansi-wl-pprint            >= 0.6.7 && < 0.7-                    , base64-bytestring         >= 1 && < 1.1+                    , base64-bytestring         >= 1 && < 1.2                     , bytestring                >= 0.10.8 && < 0.11                     , case-insensitive          >= 1.2 && < 1.3                     , cassava                   >= 0.4.5 && < 0.6@@ -67,17 +67,17 @@                     , insert-ordered-containers >= 0.2.2 && < 0.3                     , interpolatedstring-perl6  >= 1 && < 1.1                     , jose                      >= 0.8.1 && < 0.9-                    , lens                      >= 4.14 && < 4.19+                    , lens                      >= 4.14 && < 4.20                     , lens-aeson                >= 1.0.1 && < 1.2-                    , network-uri               >= 2.6.1 && < 2.7+                    , network-uri               >= 2.6.1 && < 2.8                     , optparse-applicative      >= 0.13 && < 0.16                     , parsec                    >= 3.1.11 && < 3.2-                    , protolude                 >= 0.2.2 && < 0.3+                    , protolude                 >= 0.3 && < 0.4                     , regex-tdfa                >= 1.2.2 && < 1.4                     , scientific                >= 0.3.4 && < 0.4-                    , swagger2                  >= 2.4 && < 2.6+                    , swagger2                  >= 2.4 && < 2.7                     , text                      >= 1.2.2 && < 1.3-                    , time                      >= 1.6 && < 1.10+                    , time                      >= 1.6 && < 1.11                     , unordered-containers      >= 0.2.8 && < 0.3                     , vector                    >= 0.11 && < 0.13                     , wai                       >= 3.2.1 && < 3.3@@ -88,13 +88,24 @@   default-extensions: OverloadedStrings                       QuasiQuotes                       NoImplicitPrelude+  if flag(FailOnWarn)+    ghc-options: -O2 -Werror -Wall -fwarn-identities+                 -fno-spec-constr -optP-Wno-nonportable-include-path+  else+    ghc-options: -O2 -Wall -fwarn-identities+                 -fno-spec-constr -optP-Wno-nonportable-include-path+          -- -fno-spec-constr may help keep compile time memory use in check,+          --   see https://gitlab.haskell.org/ghc/ghc/issues/16017#note_219304+          -- -optP-Wno-nonportable-include-path+          --   prevents build failures on case-insensitive filesystems (macos),+          --   see https://github.com/commercialhaskell/stack/issues/3918  executable postgrest   main-is:            Main.hs   hs-source-dirs:     main-  build-depends:      base              >= 4.9 && < 4.14+  build-depends:      base              >= 4.9 && < 4.15                     , auto-update       >= 0.1.4 && < 0.2-                    , base64-bytestring >= 1 && < 1.1+                    , base64-bytestring >= 1 && < 1.2                     , bytestring        >= 0.10.8 && < 0.11                     , directory         >= 1.2.6 && < 1.4                     , either            >= 4.4.1 && < 5.1@@ -103,17 +114,24 @@                     , hasql-transaction >= 0.7.2 && < 1.1                     , network           < 3.2                     , postgrest-                    , protolude         >= 0.2.2 && < 0.3+                    , protolude         >= 0.3 && < 0.4                     , retry             >= 0.7.4 && < 0.9                     , text              >= 1.2.2 && < 1.3-                    , time              >= 1.6 && < 1.10+                    , time              >= 1.6 && < 1.11                     , wai               >= 3.2.1 && < 3.3                     , warp              >= 3.2.12 && < 3.4   default-language:   Haskell2010   default-extensions: OverloadedStrings                       QuasiQuotes                       NoImplicitPrelude-  ghc-options:        -threaded -rtsopts "-with-rtsopts=-N -I2"+  if flag(FailOnWarn)+    ghc-options:      -threaded -rtsopts "-with-rtsopts=-N -I2"+                      -O2 -Werror -Wall -fwarn-identities+                      -fno-spec-constr -optP-Wno-nonportable-include-path+  else+    ghc-options:      -threaded -rtsopts "-with-rtsopts=-N -I2"+                      -O2 -Wall -fwarn-identities+                      -fno-spec-constr -optP-Wno-nonportable-include-path    if !os(windows)     build-depends: unix@@ -154,12 +172,12 @@                       SpecHelper                       TestTypes   hs-source-dirs:     test-  build-depends:      base              >= 4.9 && < 4.14-                    , aeson             >= 0.11.3 && < 1.5+  build-depends:      base              >= 4.9 && < 4.15+                    , aeson             >= 1.4.7 && < 1.5                     , aeson-qq          >= 0.8.1 && < 0.9                     , async             >= 2.1.1 && < 2.3                     , auto-update       >= 0.1.4 && < 0.2-                    , base64-bytestring >= 1 && < 1.1+                    , base64-bytestring >= 1 && < 1.2                     , bytestring        >= 0.10.8 && < 0.11                     , case-insensitive  >= 1.2 && < 1.3                     , cassava           >= 0.4.5 && < 0.6@@ -173,15 +191,15 @@                     , hspec-wai         >= 0.10 && < 0.11                     , hspec-wai-json    >= 0.10 && < 0.11                     , http-types        >= 0.12.3 && < 0.13-                    , lens              >= 4.14 && < 4.19+                    , lens              >= 4.14 && < 4.20                     , lens-aeson        >= 1.0.1 && < 1.2                     , monad-control     >= 1.0.1 && < 1.1                     , postgrest                     , process           >= 1.4.2 && < 1.7-                    , protolude         >= 0.2.2 && < 0.3+                    , protolude         >= 0.3 && < 0.4                     , regex-tdfa        >= 1.2.2 && < 1.4                     , text              >= 1.2.2 && < 1.3-                    , time              >= 1.6 && < 1.10+                    , time              >= 1.6 && < 1.11                     , transformers-base >= 0.4.4 && < 0.5                     , wai               >= 3.2.1 && < 3.3                     , wai-extra         >= 3.0.19 && < 3.1@@ -198,12 +216,12 @@   Hs-Source-Dirs:      test   Main-Is:             QueryCost.hs   Other-Modules:       SpecHelper-  Build-Depends:       base              >= 4.9 && < 4.14-                     , aeson             >= 0.11.3 && < 1.5+  Build-Depends:       base              >= 4.9 && < 4.15+                     , aeson             >= 1.4.7 && < 1.5                      , aeson-qq          >= 0.8.1 && < 0.9                      , async             >= 2.1.1 && < 2.3                      , auto-update       >= 0.1.4 && < 0.2-                     , base64-bytestring >= 1 && < 1.1+                     , base64-bytestring >= 1 && < 1.2                      , bytestring        >= 0.10.8 && < 0.11                      , case-insensitive  >= 1.2 && < 1.3                      , cassava           >= 0.4.5 && < 0.6@@ -217,15 +235,15 @@                      , hspec-wai         >= 0.10 && < 0.11                      , hspec-wai-json    >= 0.10 && < 0.11                      , http-types        >= 0.12.3 && < 0.13-                     , lens              >= 4.14 && < 4.19+                     , lens              >= 4.14 && < 4.20                      , lens-aeson        >= 1.0.1 && < 1.2                      , monad-control     >= 1.0.1 && < 1.1                      , postgrest                      , process           >= 1.4.2 && < 1.7-                     , protolude         >= 0.2.2 && < 0.3+                     , protolude         >= 0.3 && < 0.4                      , regex-tdfa        >= 1.2.2 && < 1.4                      , text              >= 1.2.2 && < 1.3-                     , time              >= 1.6 && < 1.10+                     , time              >= 1.6 && < 1.11                      , transformers-base >= 0.4.4 && < 0.5                      , wai               >= 3.2.1 && < 3.3                      , wai-extra         >= 3.0.19 && < 3.1
src/PostgREST/ApiRequest.hs view
@@ -28,8 +28,8 @@  import Control.Arrow             ((***)) import Data.Aeson.Types          (emptyArray, emptyObject)-import Data.List                 (elem, last, lookup, partition)-import Data.List.NonEmpty        (NonEmpty, head)+import Data.List                 (last, lookup, partition)+import Data.List.NonEmpty        (head) import Data.Maybe                (fromJust) import Data.Ranged.Ranges        (Range (..), emptyRange,                                   rangeIntersection)@@ -49,7 +49,8 @@                              rangeLimit, rangeOffset, rangeRequested,                              restrictRange) import PostgREST.Types-import Protolude            hiding (head)+import Protolude            hiding (head, toS)+import Protolude.Conv       (toS)  type RequestBody = BL.ByteString @@ -190,11 +191,10 @@       (CTOther "application/x-www-form-urlencoded", _) ->         let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody             keys = S.fromList $ M.keys json in-        Right $ ProcessedJSON (JSON.encode json) PJObject keys+        Right $ ProcessedJSON (JSON.encode json) keys       (ct, _) ->         Left $ toS $ "Content-Type not acceptable: " <> toMime ct-  rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams)-                  PJObject (S.fromList $ fst <$> rpcQParams)+  rpcPrmsToJson = ProcessedJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) (S.fromList $ fst <$> rpcQParams)   topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges -- if no limit is specified, get all the request rows   action =     case method of@@ -219,9 +219,9 @@   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+    | action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionDelete, ActionInvoke InvPost] -- 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,+    | action `elem` [ActionRead True, ActionRead False, ActionInvoke InvGet, ActionInvoke InvHead,                      ActionInspect False, ActionInspect True, ActionInfo]       = Just $ maybe defaultSchema toS $ lookupHeader "Accept-Profile"     | otherwise = Nothing@@ -333,14 +333,14 @@                 JSON.Object x -> S.fromList (M.keys x) == canonicalKeys                 _ -> False) arr in           if areKeysUniform-            then Just $ ProcessedJSON raw (PJArray $ V.length arr) canonicalKeys+            then Just $ ProcessedJSON raw canonicalKeys             else Nothing         Just _ -> Nothing         Nothing -> Just emptyPJArray -    JSON.Object o -> Just $ ProcessedJSON raw PJObject (S.fromList $ M.keys o)+    JSON.Object o -> Just $ ProcessedJSON raw (S.fromList $ M.keys o)      -- truncate everything else to an empty array.     _ -> Just emptyPJArray   where-    emptyPJArray = ProcessedJSON (JSON.encode emptyArray) (PJArray 0) S.empty+    emptyPJArray = ProcessedJSON (JSON.encode emptyArray) S.empty
src/PostgREST/App.hs view
@@ -40,13 +40,13 @@ import Network.Wai  import PostgREST.ApiRequest       (Action (..), ApiRequest (..),-                                   ContentType (..),                                    InvokeMethod (..), Target (..),                                    mutuallyAgreeable, userApiRequest) import PostgREST.Auth             (containsRole, jwtClaims,                                    parseSecret) import PostgREST.Config           (AppConfig (..))-import PostgREST.DbRequestBuilder (mutateRequest, readRequest)+import PostgREST.DbRequestBuilder (mutateRequest, readRequest,+                                   returningCols) import PostgREST.DbStructure import PostgREST.Error            (PgError (..), SimpleError (..),                                    errorResponseFor, singularityError)@@ -64,7 +64,8 @@                                    createReadStatement,                                    createWriteStatement) import PostgREST.Types-import Protolude                  hiding (Proxy, intercalate)+import Protolude                  hiding (Proxy, intercalate, toS)+import Protolude.Conv             (toS)  postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application postgrest conf refDbStructure pool getTime worker =@@ -127,7 +128,7 @@         (ActionRead headersOnly, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->           case readSqlParts tSchema tName of             Left errorResponse -> return errorResponse-            Right (q, cq, bField) -> do+            Right (q, cq, bField, _) -> do               let cQuery = if estimatedCount                              then limitedQuery cq ((+ 1) <$> maxRows) -- LIMIT maxRows + 1 so we can determine below that maxRows was surpassed                              else cq@@ -180,7 +181,7 @@                         , 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+                            else (\x -> ("Preference-Applied", encodeUtf8 (show x))) <$> iPreferResolution apiRequest                         ] ++ ctHeaders)) (unwrapGucHeader <$> ghdrs)                   if contentType == CTSingularJSON && queryTotal /= 1                     then do@@ -218,22 +219,14 @@                     else                       return $ responseLBS status headers rBody -        (ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just ProcessedJSON{pjRaw, pjType, pjKeys}) ->+        (ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just pJson) ->           case mutateSqlParts tSchema tName of             Left errorResponse -> return errorResponse-            Right (sq, mq) -> do-              let isSingle = case pjType of-                               PJArray len -> len == 1-                               PJObject    -> True-                  colNames = colName <$> tableCols dbStructure tSchema tName+            Right (sq, mq) ->               if topLevelRange /= allRange                 then return . errorResponseFor $ PutRangeNotAllowedError-              else if not isSingle-                then return . errorResponseFor $ PutSingletonError-              else if S.fromList colNames /= pjKeys-                then return . errorResponseFor $ PutPayloadIncompleteError               else do-                row <- H.statement (toS pjRaw) $+                row <- H.statement (toS $ pjRaw pJson) $                        createWriteStatement sq mq (contentType == CTSingularJSON) False                                             (contentType == CTTextCSV) (iPreferRepresentation apiRequest) [] pgVer                 let (_, queryTotal, _, body, gucHeaders) = row@@ -293,10 +286,10 @@           let tName = fromMaybe pName $ procTableName =<< proc in           case readSqlParts tSchema tName of             Left errorResponse -> return errorResponse-            Right (q, cq, bField) -> do+            Right (q, cq, bField, returning) -> do               let                 preferParams = iPreferParameters apiRequest-                pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams+                pq = requestToCallProcQuery qi (specifiedProcArgs cols proc) returnsScalar preferParams returning                 stm = callProcStatement returnsScalar pq q cq shouldCount (contentType == CTSingularJSON)                         (contentType == CTTextCSV) (contentType `elem` rawContentTypes) (preferParams == Just MultipleObjects)                         bField pgVer@@ -351,11 +344,14 @@         readSqlParts s t =           let             readReq = readRequest s t maxRows (dbRelations dbStructure) apiRequest+            returnings :: ReadRequest -> Either Response [FieldName]+            returnings rr = Right (returningCols rr)           in-          (,,) <$>+          (,,,) <$>           (readRequestToQuery <$> readReq) <*>           (readRequestToCountQuery <$> readReq) <*>-          (binaryField contentType rawContentTypes returnsScalar =<< readReq)+          (binaryField contentType rawContentTypes returnsScalar =<< readReq) <*>+          (returnings =<< readReq)          mutateSqlParts s t =           let
src/PostgREST/Auth.hs view
@@ -31,7 +31,8 @@ import Crypto.JWT  import PostgREST.Types-import Protolude+import Protolude       hiding (toS)+import Protolude.Conv  (toS)  {-|   Possible situations encountered with client JWTs
src/PostgREST/Config.hs view
@@ -32,17 +32,15 @@ import qualified Data.Configurator            as C import qualified Text.PrettyPrint.ANSI.Leijen as L -import Control.Exception           (Handler (..)) 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.List.NonEmpty          (fromList) import Data.Scientific             (floatingOrInteger) import Data.Text                   (dropEnd, dropWhileEnd,-                                    intercalate, lines, splitOn,-                                    strip, take, unpack)-import Data.Text.Encoding          (encodeUtf8)+                                    intercalate, splitOn, strip, take,+                                    unpack) import Data.Text.IO                (hPutStrLn) import Data.Version                (versionBranch) import Development.GitRev          (gitHash)@@ -63,8 +61,8 @@ import PostgREST.Parsers (pRoleClaimKey) import PostgREST.Types   (JSPath, JSPathExp (..)) import Protolude         hiding (concat, hPutStrLn, intercalate, null,-                          take, (<>))-+                          take, toS, (<>))+import Protolude.Conv    (toS)   -- | Config file settings for the server@@ -155,7 +153,7 @@       AppConfig         <$> reqString "db-uri"         <*> reqString "db-anon-role"-        <*> optString "server-proxy-uri"+        <*> optString "openapi-server-proxy-uri"         <*> (fromList . splitOnCommas <$> reqValue "db-schema")         <*> (fromMaybe "!4" <$> optString "server-host")         <*> (fromMaybe 3000 <$> optInt "server-port")
src/PostgREST/DbRequestBuilder.hs view
@@ -14,6 +14,7 @@ module PostgREST.DbRequestBuilder (   readRequest , mutateRequest+, returningCols ) where  import qualified Data.HashMap.Strict as M@@ -40,7 +41,7 @@ readRequest schema rootTableName maxRows allRels apiRequest  =   mapLeft errorResponseFor $   treeRestrictRange maxRows =<<-  augumentRequestWithJoin schema rootRels =<<+  augmentRequestWithJoin schema rootRels =<<   addFiltersOrdersRanges apiRequest <*>   (initReadRequest rootName <$> pRequestSelect sel)   where@@ -89,8 +90,8 @@     nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode     nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i) -augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest-augumentRequestWithJoin schema allRels request =+augmentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest+augmentRequestWithJoin schema allRels request =   addRels schema allRels Nothing request   >>= addJoinConditions Nothing 
src/PostgREST/DbStructure.hs view
@@ -35,14 +35,13 @@ import Data.Text                     (breakOn, dropAround, split,                                       splitOn, strip) import GHC.Exts                      (groupWith)+import Protolude                     hiding (toS)+import Protolude.Conv                (toS)+import Protolude.Unsafe              (unsafeHead) import Text.InterpolatedString.Perl6 (q, qc)-import Unsafe                        (unsafeHead) -import Control.Applicative- import PostgREST.Private.Common import PostgREST.Types-import Protolude  getDbStructure :: [Schema] -> PgVersion -> HT.Transaction DbStructure getDbStructure schemas pgVer = do@@ -190,14 +189,14 @@ procsSqlQuery :: SqlQuery procsSqlQuery = [q|   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",+    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@@ -229,11 +228,22 @@       n.nspname as table_schema,       relname as table_name,       d.description as table_description,-      c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8-      or (exists (-         select 1-         from pg_trigger-         where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69)+      (+        c.relkind in ('r', 'v', 'f')+        and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8+        -- The function `pg_relation_is_updateable` returns a bitmask where 8+        -- corresponds to `1 << CMD_INSERT` in the PostgreSQL source code, i.e.+        -- it's possible to insert into the relation.+        or (exists (+          select 1+          from pg_trigger+          where+            pg_trigger.tgrelid = c.oid+            and (pg_trigger.tgtype::integer & 69) = 69)+            -- The trigger type `tgtype` is a bitmask where 69 corresponds to+            -- TRIGGER_TYPE_ROW + TRIGGER_TYPE_INSTEAD + TRIGGER_TYPE_INSERT+            -- in the PostgreSQL source code.+        )       ) as insertable     from       pg_class c@@ -243,9 +253,9 @@       c.relkind in ('v', 'r', 'm', 'f')       and n.nspname = $1       and (-        pg_has_role(c.relowner, 'USAGE'::text)-        or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text)-        or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)+        pg_has_role(c.relowner, 'USAGE')+        or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER')+        or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES')       )     order by relname |] @@ -374,13 +384,17 @@       n.nspname AS table_schema,       c.relname AS table_name,       NULL AS table_description,-      c.relkind = 'r' OR (c.relkind IN ('v','f'))-      AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8-      OR (EXISTS-        ( SELECT 1+      (+        c.relkind IN ('r', 'v','f')+        AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8+        OR EXISTS (+          SELECT 1           FROM pg_trigger-          WHERE pg_trigger.tgrelid = c.oid-          AND (pg_trigger.tgtype::integer & 69) = 69) ) AS insertable+          WHERE+            pg_trigger.tgrelid = c.oid+            AND (pg_trigger.tgtype::integer & 69) = 69+        )+      ) AS insertable     FROM pg_class c     JOIN pg_namespace n ON n.oid = c.relnamespace     WHERE c.relkind IN ('v','r','m','f')@@ -407,9 +421,7 @@         info.column_default AS default_value,         array_to_string(enum_info.vals, ',') AS enum     FROM (-        /*         -- CTE based on pg_catalog to get PRIMARY/FOREIGN key and UNIQUE columns outside api schema-        */         WITH key_columns AS (              SELECT                r.oid AS r_oid,@@ -437,19 +449,16 @@         -- limit columns to the ones in the api schema or PK/FK columns         */         columns AS (-            SELECT current_database()::information_schema.sql_identifier AS table_catalog,-                nc.nspname::information_schema.sql_identifier AS table_schema,-                c.relname::information_schema.sql_identifier AS table_name,-                a.attname::information_schema.sql_identifier AS column_name,+            SELECT+                nc.nspname::name AS table_schema,+                c.relname::name AS table_name,+                a.attname::name AS column_name,                 d.description AS description,-                a.attnum::information_schema.cardinal_number AS ordinal_position,-                pg_get_expr(ad.adbin, ad.adrelid)::information_schema.character_data AS column_default,-                    CASE-                        WHEN a.attnotnull OR t.typtype = 'd'::"char" AND t.typnotnull THEN 'NO'::text-                        ELSE 'YES'::text-                    END::information_schema.yes_or_no AS is_nullable,+                a.attnum::integer AS ordinal_position,+                pg_get_expr(ad.adbin, ad.adrelid)::text AS column_default,+                not (a.attnotnull OR t.typtype = 'd' AND t.typnotnull) AS is_nullable,                     CASE-                        WHEN t.typtype = 'd'::"char" THEN+                        WHEN t.typtype = 'd' THEN                         CASE                             WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text                             WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)@@ -461,77 +470,42 @@                             WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)                             ELSE format_type(a.atttypid, a.atttypmod)                         END-                    END::information_schema.character_data AS data_type,-                information_schema._pg_char_max_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_maximum_length,-                information_schema._pg_char_octet_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_octet_length,-                information_schema._pg_numeric_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision,-                information_schema._pg_numeric_precision_radix(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision_radix,-                information_schema._pg_numeric_scale(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_scale,-                information_schema._pg_datetime_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS datetime_precision,-                information_schema._pg_interval_type(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.character_data AS interval_type,-                NULL::integer::information_schema.cardinal_number AS interval_precision,-                NULL::character varying::information_schema.sql_identifier AS character_set_catalog,-                NULL::character varying::information_schema.sql_identifier AS character_set_schema,-                NULL::character varying::information_schema.sql_identifier AS character_set_name,-                    CASE-                        WHEN nco.nspname IS NOT NULL THEN current_database()-                        ELSE NULL::name-                    END::information_schema.sql_identifier AS collation_catalog,-                nco.nspname::information_schema.sql_identifier AS collation_schema,-                co.collname::information_schema.sql_identifier AS collation_name,-                    CASE-                        WHEN t.typtype = 'd'::"char" THEN current_database()-                        ELSE NULL::name-                    END::information_schema.sql_identifier AS domain_catalog,-                    CASE-                        WHEN t.typtype = 'd'::"char" THEN nt.nspname-                        ELSE NULL::name-                    END::information_schema.sql_identifier AS domain_schema,-                    CASE-                        WHEN t.typtype = 'd'::"char" THEN t.typname-                        ELSE NULL::name-                    END::information_schema.sql_identifier AS domain_name,-                current_database()::information_schema.sql_identifier AS udt_catalog,-                COALESCE(nbt.nspname, nt.nspname)::information_schema.sql_identifier AS udt_schema,-                COALESCE(bt.typname, t.typname)::information_schema.sql_identifier AS udt_name,-                NULL::character varying::information_schema.sql_identifier AS scope_catalog,-                NULL::character varying::information_schema.sql_identifier AS scope_schema,-                NULL::character varying::information_schema.sql_identifier AS scope_name,-                NULL::integer::information_schema.cardinal_number AS maximum_cardinality,-                a.attnum::information_schema.sql_identifier AS dtd_identifier,-                'NO'::character varying::information_schema.yes_or_no AS is_self_referencing,-                'NO'::character varying::information_schema.yes_or_no AS is_identity,-                NULL::character varying::information_schema.character_data AS identity_generation,-                NULL::character varying::information_schema.character_data AS identity_start,-                NULL::character varying::information_schema.character_data AS identity_increment,-                NULL::character varying::information_schema.character_data AS identity_maximum,-                NULL::character varying::information_schema.character_data AS identity_minimum,-                NULL::character varying::information_schema.yes_or_no AS identity_cycle,-                'NEVER'::character varying::information_schema.character_data AS is_generated,-                NULL::character varying::information_schema.character_data AS generation_expression,-                CASE-                    WHEN c.relkind = 'r'::"char" OR (c.relkind = ANY (ARRAY['v'::"char", 'f'::"char"])) AND pg_column_is_updatable(c.oid::regclass, a.attnum, false) THEN 'YES'::text-                    ELSE 'NO'::text-                END::information_schema.yes_or_no AS is_updatable+                    END::text AS data_type,+                information_schema._pg_char_max_length(+                    information_schema._pg_truetypid(a.*, t.*),+                    information_schema._pg_truetypmod(a.*, t.*)+                )::integer AS character_maximum_length,+                information_schema._pg_numeric_precision(+                    information_schema._pg_truetypid(a.*, t.*),+                    information_schema._pg_truetypmod(a.*, t.*)+                )::integer AS numeric_precision,+                COALESCE(bt.typname, t.typname)::name AS udt_name,+                (+                    c.relkind in ('r', 'v', 'f')+                    AND pg_column_is_updatable(c.oid::regclass, a.attnum, false)+                )::bool is_updatable             FROM pg_attribute a-               LEFT JOIN key_columns kc ON kc.conkey = a.attnum AND kc.c_oid = a.attrelid-               LEFT JOIN pg_catalog.pg_description AS d ON d.objoid = a.attrelid and d.objsubid = a.attnum-               LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum-               JOIN (pg_class c-               JOIN pg_namespace nc ON c.relnamespace = nc.oid) ON a.attrelid = c.oid-               JOIN (pg_type t-               JOIN pg_namespace nt ON t.typnamespace = nt.oid) ON a.atttypid = t.oid-               LEFT JOIN (pg_type bt-               JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid) ON t.typtype = 'd'::"char" AND t.typbasetype = bt.oid-               LEFT JOIN (pg_collation co-               JOIN pg_namespace nco ON co.collnamespace = nco.oid) ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)+                LEFT JOIN key_columns kc+                    ON kc.conkey = a.attnum AND kc.c_oid = a.attrelid+                LEFT JOIN pg_catalog.pg_description AS d+                    ON d.objoid = a.attrelid and d.objsubid = a.attnum+                LEFT JOIN pg_attrdef ad+                    ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum+                JOIN (pg_class c JOIN pg_namespace nc ON c.relnamespace = nc.oid)+                    ON a.attrelid = c.oid+                JOIN (pg_type t JOIN pg_namespace nt ON t.typnamespace = nt.oid)+                    ON a.atttypid = t.oid+                LEFT JOIN (pg_type bt JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid)+                    ON t.typtype = 'd' AND t.typbasetype = bt.oid+                LEFT JOIN (pg_collation co JOIN pg_namespace nco ON co.collnamespace = nco.oid)+                    ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)             WHERE                 NOT pg_is_other_temp_schema(nc.oid)                 AND a.attnum > 0                 AND NOT a.attisdropped-                AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char", 'm'::"char"]))-                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))*/+                AND c.relkind in ('r', 'v', 'f', 'm')+                -- Filter only columns that are FK/PK or in the api schema:+                AND (nc.nspname = ANY ($1) OR kc.r_oid IS NOT NULL)         )         SELECT             table_schema,@@ -546,7 +520,6 @@             numeric_precision,             column_default,             udt_name-        /*-- FROM information_schema.columns*/         FROM columns         WHERE table_schema NOT IN ('pg_catalog', 'information_schema')     ) AS info@@ -618,69 +591,36 @@   H.Statement sql HE.noParams (decodePks tabs) True  where   sql = [q|-    /*     -- CTE to replace information_schema.table_constraints to remove owner limit-    */     WITH tc AS (-        SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,-            nc.nspname::information_schema.sql_identifier AS constraint_schema,-            c.conname::information_schema.sql_identifier AS constraint_name,-            current_database()::information_schema.sql_identifier AS table_catalog,-            nr.nspname::information_schema.sql_identifier AS table_schema,-            r.relname::information_schema.sql_identifier AS table_name,-                CASE c.contype-                    WHEN 'c'::"char" THEN 'CHECK'::text-                    WHEN 'f'::"char" THEN 'FOREIGN KEY'::text-                    WHEN 'p'::"char" THEN 'PRIMARY KEY'::text-                    WHEN 'u'::"char" THEN 'UNIQUE'::text-                    ELSE NULL::text-                END::information_schema.character_data AS constraint_type,-                CASE-                    WHEN c.condeferrable THEN 'YES'::text-                    ELSE 'NO'::text-                END::information_schema.yes_or_no AS is_deferrable,-                CASE-                    WHEN c.condeferred THEN 'YES'::text-                    ELSE 'NO'::text-                END::information_schema.yes_or_no AS initially_deferred+        SELECT+            c.conname::name AS constraint_name,+            nr.nspname::name AS table_schema,+            r.relname::name AS table_name         FROM pg_namespace nc,             pg_namespace nr,             pg_constraint c,             pg_class r-        WHERE nc.oid = c.connamespace AND nr.oid = r.relnamespace AND c.conrelid = r.oid AND (c.contype <> ALL (ARRAY['t'::"char", 'x'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)-        /*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/-        UNION ALL-        SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,-            nr.nspname::information_schema.sql_identifier AS constraint_schema,-            (((((nr.oid::text || '_'::text) || r.oid::text) || '_'::text) || a.attnum::text) || '_not_null'::text)::information_schema.sql_identifier AS constraint_name,-            current_database()::information_schema.sql_identifier AS table_catalog,-            nr.nspname::information_schema.sql_identifier AS table_schema,-            r.relname::information_schema.sql_identifier AS table_name,-            'CHECK'::character varying::information_schema.character_data AS constraint_type,-            'NO'::character varying::information_schema.yes_or_no AS is_deferrable,-            'NO'::character varying::information_schema.yes_or_no AS initially_deferred-        FROM pg_namespace nr,-            pg_class r,-            pg_attribute a-        WHERE nr.oid = r.relnamespace AND r.oid = a.attrelid AND a.attnotnull AND a.attnum > 0 AND NOT a.attisdropped AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)-        /*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/+        WHERE+            nc.oid = c.connamespace+            AND nr.oid = r.relnamespace+            AND c.conrelid = r.oid+            AND r.relkind = 'r'+            AND NOT pg_is_other_temp_schema(nr.oid)+            AND c.contype = 'p'     ),-    /*     -- CTE to replace information_schema.key_column_usage to remove owner limit-    */     kc AS (-        SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,-            ss.nc_nspname::information_schema.sql_identifier AS constraint_schema,-            ss.conname::information_schema.sql_identifier AS constraint_name,-            current_database()::information_schema.sql_identifier AS table_catalog,-            ss.nr_nspname::information_schema.sql_identifier AS table_schema,-            ss.relname::information_schema.sql_identifier AS table_name,-            a.attname::information_schema.sql_identifier AS column_name,-            (ss.x).n::information_schema.cardinal_number AS ordinal_position,-                CASE-                    WHEN ss.contype = 'f'::"char" THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])-                    ELSE NULL::integer-                END::information_schema.cardinal_number AS position_in_unique_constraint+        SELECT+            ss.conname::name AS constraint_name,+            ss.nr_nspname::name AS table_schema,+            ss.relname::name AS table_name,+            a.attname::name AS column_name,+            (ss.x).n::integer AS ordinal_position,+            CASE+                WHEN ss.contype = 'f' THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])+                ELSE NULL::integer+            END::integer AS position_in_unique_constraint         FROM pg_attribute a,             ( SELECT r.oid AS roid,                 r.relname,@@ -692,28 +632,31 @@                 c.contype,                 c.conindid,                 c.confkey,-                c.confrelid,                 information_schema._pg_expandarray(c.conkey) AS x                FROM pg_namespace nr,                 pg_class r,                 pg_namespace nc,                 pg_constraint c-              WHERE nr.oid = r.relnamespace AND r.oid = c.conrelid AND nc.oid = c.connamespace AND (c.contype = ANY (ARRAY['p'::"char", 'u'::"char", 'f'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)) ss-        WHERE ss.roid = a.attrelid AND a.attnum = (ss.x).x AND NOT a.attisdropped-        /*--AND (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/+              WHERE+                nr.oid = r.relnamespace+                AND r.oid = c.conrelid+                AND nc.oid = c.connamespace+                AND c.contype in ('p', 'u', 'f')+                AND r.relkind = 'r'+                AND NOT pg_is_other_temp_schema(nr.oid)+            ) ss+        WHERE+          ss.roid = a.attrelid+          AND a.attnum = (ss.x).x+          AND NOT a.attisdropped     )     SELECT         kc.table_schema,         kc.table_name,         kc.column_name     FROM-        /*-        --information_schema.table_constraints tc,-        --information_schema.key_column_usage kc-        */         tc, kc     WHERE-        tc.constraint_type = 'PRIMARY KEY' AND         kc.table_name = tc.table_name AND         kc.table_schema = tc.table_schema AND         kc.constraint_name = tc.constraint_name AND@@ -744,7 +687,7 @@         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 = ANY ($1)+        where c.relkind in ('v', 'm') and n.nspname = ANY ($1)       ),       removed_subselects as(         select
src/PostgREST/Error.hs view
@@ -23,12 +23,12 @@  import Data.Aeson  ((.=)) import Network.Wai (Response, responseLBS)-import Text.Read   (readMaybe)  import Network.HTTP.Types.Header  import PostgREST.Types-import Protolude+import Protolude       hiding (toS)+import Protolude.Conv  (toS)   class (JSON.ToJSON a) => PgrstError a where@@ -223,10 +223,8 @@   = GucHeadersError   | BinaryFieldError ContentType   | ConnectionLostError-  | PutSingletonError   | PutMatchingPkError   | PutRangeNotAllowedError-  | PutPayloadIncompleteError   | JwtTokenMissing   | JwtTokenInvalid Text   | SingularityError Integer@@ -234,17 +232,15 @@   deriving (Show, Eq)  instance PgrstError SimpleError where-  status GucHeadersError           = HT.status500-  status (BinaryFieldError _)      = HT.status406-  status ConnectionLostError       = HT.status503-  status PutSingletonError         = HT.status400-  status PutMatchingPkError        = HT.status400-  status PutRangeNotAllowedError   = HT.status400-  status PutPayloadIncompleteError = HT.status400-  status JwtTokenMissing           = HT.status500-  status (JwtTokenInvalid _)       = HT.unauthorized401-  status (SingularityError _)      = HT.status406-  status (ContentTypeError _)      = HT.status415+  status GucHeadersError         = HT.status500+  status (BinaryFieldError _)    = HT.status406+  status ConnectionLostError     = HT.status503+  status PutMatchingPkError      = HT.status400+  status PutRangeNotAllowedError = HT.status400+  status JwtTokenMissing         = HT.status500+  status (JwtTokenInvalid _)     = HT.unauthorized401+  status (SingularityError _)    = HT.status406+  status (ContentTypeError _)    = HT.status415    headers (SingularityError _)     = [toHeader CTSingularJSON]   headers (JwtTokenInvalid m)      = [toHeader CTApplicationJSON, invalidTokenHeader m]@@ -258,12 +254,8 @@   toJSON ConnectionLostError       = JSON.object [     "message" .= ("Database connection lost, retrying the connection." :: Text)] -  toJSON PutSingletonError         = JSON.object [-    "message" .= ("PUT payload must contain a single row" :: Text)]   toJSON PutRangeNotAllowedError   = JSON.object [     "message" .= ("Range header and limit/offset querystring parameters are not allowed for PUT" :: Text)]-  toJSON PutPayloadIncompleteError = JSON.object [-    "message" .= ("You must specify all columns in the payload when using PUT" :: Text)]   toJSON PutMatchingPkError        = JSON.object [     "message" .= ("Payload values do not match URL in primary key column(s)" :: Text)] @@ -280,7 +272,7 @@  invalidTokenHeader :: Text -> Header invalidTokenHeader m =-  ("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> show m)+  ("WWW-Authenticate", "Bearer error=\"invalid_token\", " <> "error_description=" <> encodeUtf8 (show m))  singularityError :: (Integral a) => a -> SimpleError singularityError = SingularityError . toInteger
src/PostgREST/Middleware.hs view
@@ -27,7 +27,8 @@ import PostgREST.Error        (SimpleError (JwtTokenInvalid, JwtTokenMissing),                                errorResponseFor) import PostgREST.QueryBuilder (setLocalQuery, setLocalSearchPathQuery)-import Protolude              hiding (head)+import Protolude              hiding (head, toS)+import Protolude.Conv         (toS)  runWithClaims :: AppConfig -> JWTAttempt ->                  (ApiRequest -> H.Transaction Response) ->
src/PostgREST/OpenAPI.hs view
@@ -32,7 +32,8 @@                              PrimaryKey (..), ProcDescription (..),                              Proxy (..), Table (..), toMime) import Protolude            hiding (Proxy, dropWhile, get,-                             intercalate, (&))+                             intercalate, toLower, toS, (&))+import Protolude.Conv       (toS)  makeMimeList :: [ContentType] -> MimeList makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs@@ -117,7 +118,7 @@   [ Inline $ (mempty :: Param)     & name     .~ "args"     & required ?~ True-    & schema   .~ (ParamBody $ Inline $ makeProcSchema pd)+    & schema   .~ ParamBody (Inline $ makeProcSchema pd)   , Ref $ Reference "preferParams"   ] @@ -221,7 +222,7 @@         & description .~ "OK"         & schema ?~ Inline (mempty           & type_ ?~ SwaggerArray-          & items ?~ (SwaggerItemsObject $ Ref $ Reference $ tableName t)+          & items ?~ SwaggerItemsObject (Ref $ Reference $ tableName t)         )       )     postOp = tOp
src/PostgREST/Parsers.hs view
@@ -9,10 +9,8 @@ import qualified Data.HashMap.Strict as M import qualified Data.Set            as S -import Control.Monad           ((>>)) import Data.Either.Combinators (mapLeft) import Data.Foldable           (foldl1)-import Data.Functor            (($>)) import Data.List               (init, last) import Data.Text               (intercalate, replace, strip) import Text.Read               (read)@@ -24,7 +22,9 @@ import PostgREST.Error      (ApiRequestError (ParseRequestError)) import PostgREST.RangeQuery (NonnegRange) import PostgREST.Types-import Protolude            hiding (intercalate, option, replace, try)+import Protolude            hiding (intercalate, option, replace, toS,+                             try)+import Protolude.Conv       (toS)  pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem] pRequestSelect selStr =
src/PostgREST/Private/QueryFragment.hs view
@@ -11,12 +11,13 @@ import           Data.Maybe import           Data.Text                     (intercalate,                                                 isInfixOf, replace,-                                                toLower, unwords)+                                                toLower) import qualified Data.Text                     as T (map, null,                                                      takeWhile) import           PostgREST.Types import           Protolude                     hiding (cast,-                                                intercalate, replace)+                                                intercalate, replace,+                                                toLower) import           Text.InterpolatedString.Perl6 (qc)  noLocationF :: SqlFragment@@ -186,8 +187,8 @@ countF countQuery shouldCount =   if shouldCount     then (-        ", pg_source_count AS (" <> countQuery <> ")"-      , "(SELECT pg_catalog.count(*) FROM pg_source_count)" )+        ", pgrst_source_count AS (" <> countQuery <> ")"+      , "(SELECT pg_catalog.count(*) FROM pgrst_source_count)" )     else (         mempty       , "null::bigint")
src/PostgREST/QueryBuilder.hs view
@@ -22,7 +22,7 @@  import qualified Data.Set as S -import Data.Text (intercalate, unwords)+import Data.Text (intercalate) import Data.Tree (Tree (..))  import Data.Maybe@@ -94,7 +94,10 @@     cols = intercalate ", " $ pgFmtIdent <$> S.toList iCols mutateRequestToQuery (Update mainQi uCols logicForest returnings) =   if S.null uCols-    then "WITH " <> ignoredBody <> "SELECT null WHERE false" -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax+    -- if there are no columns we cannot do UPDATE table SET {empty}, it'd be invalid syntax+    -- selecting an empty resultset from mainQi gives us the column names to prevent errors when using &select=+    -- the select has to be based on "returnings" to make computed overloaded functions not throw+    then "WITH " <> ignoredBody <> "SELECT " <> empty_body_returned_columns <> " FROM " <> fromQi mainQi <> " WHERE false"     else       unwords [         "WITH " <> normalizedBody,@@ -105,6 +108,10 @@         ]   where     cols = intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList uCols)+    empty_body_returned_columns :: SqlFragment+    empty_body_returned_columns+      | null returnings = "NULL"+      | otherwise       = intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName mainQi) <$> returnings) mutateRequestToQuery (Delete mainQi logicForest returnings) =   unwords [     "WITH " <> ignoredBody,@@ -113,15 +120,15 @@     returningF mainQi returnings     ] -requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Bool -> Maybe PreferParameters -> SqlQuery-requestToCallProcQuery qi pgArgs returnsScalar preferParams =+requestToCallProcQuery :: QualifiedIdentifier -> [PgArg] -> Bool -> Maybe PreferParameters -> [FieldName] -> SqlQuery+requestToCallProcQuery qi pgArgs returnsScalar preferParams returnings =   unwords [     "WITH",     argsCTE,     sourceBody ]   where     paramsAsSingleObject    = preferParams == Just SingleObject-    paramsAsMulitpleObjects = preferParams == Just MultipleObjects+    paramsAsMultipleObjects = preferParams == Just MultipleObjects      (argsCTE, args)       | null pgArgs = (ignoredBody, "")@@ -132,7 +139,7 @@             "pgrst_args AS (",               "SELECT * FROM json_to_recordset(" <> selectBody <> ") AS _(" <> fmtArgs (\a -> " " <> pgaType a) <> ")",             ")"]-         , if paramsAsMulitpleObjects+         , if paramsAsMultipleObjects              then fmtArgs (\a -> " := pgrst_args." <> pgFmtIdent (pgaName a))              else fmtArgs (\a -> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM pgrst_args LIMIT 1)")         )@@ -142,19 +149,24 @@      sourceBody :: SqlFragment     sourceBody-      | paramsAsMulitpleObjects =+      | paramsAsMultipleObjects =           if returnsScalar             then "SELECT " <> callIt <> " AS pgrst_scalar FROM pgrst_args"             else unwords [ "SELECT pgrst_lat_args.*"                          , "FROM pgrst_args,"-                         , "LATERAL ( SELECT * FROM " <> callIt <> " ) pgrst_lat_args" ]+                         , "LATERAL ( SELECT " <> returned_columns <> " FROM " <> callIt <> " ) pgrst_lat_args" ]       | otherwise =           if returnsScalar             then "SELECT " <> callIt <> " AS pgrst_scalar"-            else "SELECT * FROM " <> callIt+            else "SELECT " <> returned_columns <> " FROM " <> callIt      callIt :: SqlFragment     callIt = fromQi qi <> "(" <> args <> ")"++    returned_columns :: SqlFragment+    returned_columns+      | null returnings = "*"+      | otherwise       = intercalate ", " (pgFmtColumn (QualifiedIdentifier mempty $ qiName qi) <$> returnings)   -- | SQL query meant for COUNTing the root node of the Tree.
src/PostgREST/RangeQuery.hs view
@@ -26,7 +26,8 @@ import Network.HTTP.Types.Header import Network.HTTP.Types.Status -import Protolude+import Protolude      hiding (toS)+import Protolude.Conv (toS)  type NonnegRange = Range Integer @@ -91,9 +92,9 @@  contentRangeH :: (Integral a, Show a) => a -> a -> Maybe a -> Header contentRangeH lower upper total =-    ("Content-Range", headerValue)+    ("Content-Range", toUtf8 headerValue)     where-      headerValue   = rangeString <> "/" <> totalString+      headerValue   = rangeString <> "/" <> totalString :: Text       rangeString         | totalNotZero && fromInRange = show lower <> "-" <> show upper         | otherwise = "*"
src/PostgREST/Statements.hs view
@@ -22,8 +22,6 @@ import qualified Data.Aeson.Lens                 as L import qualified Data.ByteString.Char8           as BS import           Data.Maybe-import           Data.Text                       (unwords)-import           Data.Text.Encoding              (encodeUtf8) import qualified Hasql.Decoders                  as HD import qualified Hasql.Encoders                  as HE import qualified Hasql.Statement                 as H@@ -31,7 +29,8 @@ import           PostgREST.Private.QueryFragment import           PostgREST.Types import           Protolude                       hiding (cast,-                                                  replace)+                                                  replace, toS)+import           Protolude.Conv                  (toS) import           Text.InterpolatedString.Perl6   (qc)  {-| The generic query result format used by API responses. The location header@@ -55,7 +54,7 @@         {locF} AS header,         {bodyF} AS body,         {responseHeadersF pgVer} AS response_headers-      FROM ({selectQuery}) _postgrest_t |]+      FROM ({selectF}) _postgrest_t |]    locF =     if isInsert && rep `elem` [Full, HeadersOnly]@@ -71,6 +70,11 @@     | asCsv = asCsvF     | wantSingle = asJsonSingleF     | otherwise = asJsonF++  selectF+    -- prevent using any of the column names in ?select= when no response is returned from the CTE+    | rep `elem` [None, HeadersOnly] = "SELECT * FROM " <> sourceCTEName+    | otherwise                      = selectQuery    decodeStandard :: HD.Result ResultsWithCount   decodeStandard =
src/PostgREST/Types.hs view
@@ -24,7 +24,8 @@ import Data.Tree  import PostgREST.RangeQuery (NonnegRange)-import Protolude+import Protolude            hiding (toS)+import Protolude.Conv       (toS)  -- | Enumeration of currently supported response content types data ContentType = CTApplicationJSON | CTSingularJSON@@ -308,9 +309,8 @@   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 #1005 for more details+    -- had high memory usage, see https://github.com/PostgREST/postgrest/pull/1005 for more details     pjRaw  :: BL.ByteString-  , pjType :: PJType     -- | 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   }|@@ -522,7 +522,7 @@ pgVersion121 = PgVersion 120001 "12.1"  sourceCTEName :: SqlFragment-sourceCTEName = "pg_source"+sourceCTEName = "pgrst_source"  -- | full jspath, e.g. .property[0].attr.detail type JSPath = [JSPathExp]
test/Feature/ConcurrentSpec.hs view
@@ -5,7 +5,6 @@ module Feature.ConcurrentSpec where  import Control.Concurrent.Async (mapConcurrently)-import Control.Monad            (void) import Network.Wai              (Application)  import Control.Monad.Base
test/Feature/DeleteSpec.hs view
@@ -27,15 +27,30 @@           { matchStatus  = 200           , matchHeaders = ["Content-Range" <:> "*/1"]           }++      it "ignores ?select= when return not set or return=minimal" $ do+        request methodDelete "/items?id=eq.3&select=id" [] ""+          `shouldRespondWith` ""+          { matchStatus  = 204+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }+        request methodDelete "/items?id=eq.3&select=id" [("Prefer", "return=minimal")] ""+          `shouldRespondWith` ""+          { matchStatus  = 204+          , matchHeaders = ["Content-Range" <:> "*/*"]+          }+       it "returns the deleted item and shapes the response" $         request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""           `shouldRespondWith` [str|[{"id":2,"name":"Two"}]|]           { matchStatus  = 200           , matchHeaders = ["Content-Range" <:> "*/*"]           }+       it "can rename and cast the selected columns" $         request methodDelete "/complex_items?id=eq.3&select=ciId:id::text,ciName:name" [("Prefer", "return=representation")] ""           `shouldRespondWith` [str|[{"ciId":"3","ciName":"Three"}]|]+       it "can embed (parent) entities" $         request methodDelete "/tasks?id=eq.8&select=id,name,project:projects(id)" [("Prefer", "return=representation")] ""           `shouldRespondWith` [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]
test/Feature/EmbedDisambiguationSpec.hs view
@@ -324,20 +324,20 @@             ]|]             { matchHeaders = [matchContentTypeJson] } -        it "embeds childs recursively" $-          get "/family_tree?id=eq.1&select=id,name, childs:family_tree!parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`+        it "embeds children recursively" $+          get "/family_tree?id=eq.1&select=id,name, children:family_tree!parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`             [json|[{-              "id": "1", "name": "Parental Unit", "childs": [-                { "id": "2", "name": "Kid One", "childs": [ { "id": "4", "name": "Grandkid One" } ] },-                { "id": "3", "name": "Kid Two", "childs": [ { "id": "5", "name": "Grandkid Two" } ] }+              "id": "1", "name": "Parental Unit", "children": [+                { "id": "2", "name": "Kid One", "children": [ { "id": "4", "name": "Grandkid One" } ] },+                { "id": "3", "name": "Kid Two", "children": [ { "id": "5", "name": "Grandkid Two" } ] }               ]             }]|] { matchHeaders = [matchContentTypeJson] } -        it "embeds parent and then embeds childs" $-          get "/family_tree?id=eq.2&select=id,name,parent(id,name,childs:family_tree!parent(id,name))" `shouldRespondWith`+        it "embeds parent and then embeds children" $+          get "/family_tree?id=eq.2&select=id,name,parent(id,name,children:family_tree!parent(id,name))" `shouldRespondWith`             [json|[{               "id": "2", "name": "Kid One", "parent": {-                "id": "1", "name": "Parental Unit", "childs": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]+                "id": "1", "name": "Parental Unit", "children": [ { "id": "2", "name": "Kid One" }, { "id": "3", "name": "Kid Two"} ]               }             }]|] { matchHeaders = [matchContentTypeJson] } @@ -356,7 +356,7 @@               }             }]|] { matchHeaders = [matchContentTypeJson] } -        it "embeds childs" $ do+        it "embeds children" $ do           get "/organizations?select=id,name,refereeds:organizations!referee(id,name)&id=eq.1" `shouldRespondWith`             [json|[{               "id": 1, "name": "Referee Org",
test/Feature/InsertSpec.hs view
@@ -2,7 +2,6 @@  import qualified Data.Aeson as JSON -import Control.Monad          (replicateM_, void) import Data.List              (lookup) import Data.Maybe             (fromJust) import Network.Wai            (Application)@@ -48,6 +47,22 @@           , matchHeaders = [matchContentTypeJson]           } +      it "ignores &select when return not set or using return=minimal" $ do+        request methodPost "/menagerie?select=integer,varchar" []+          [json| [{+            "integer": 15, "double": 3.14159, "varchar": "testing!"+          , "boolean": false, "date": "1900-01-01", "money": "$3.99"+          , "enum": "foo"+          }] |] `shouldRespondWith` ""+          { matchStatus  = 201 }+        request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=minimal")]+          [json| [{+            "integer": 16, "double": 3.14159, "varchar": "testing!"+          , "boolean": false, "date": "1900-01-01", "money": "$3.99"+          , "enum": "foo"+          }] |] `shouldRespondWith` ""+          { matchStatus  = 201 }+     context "non uniform json array" $ do       it "rejects json array that isn't exclusivily composed of objects" $         post "/articles"@@ -189,11 +204,20 @@       it "fails with 400 and error" $         post "/simple_pk" "}{ x = 2"         `shouldRespondWith`-        [json|{"message":"Error in $: Failed reading: not a valid json value"}|]+        [json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]         { matchStatus  = 400         , matchHeaders = [matchContentTypeJson]         } +    context "with no payload" $+      it "fails with 400 and error" $+        post "/simple_pk" ""+        `shouldRespondWith`+        [json|{"message":"Error in $: not enough input"}|]+        { matchStatus  = 400+        , matchHeaders = [matchContentTypeJson]+        }+     context "with valid json payload" $       it "succeeds and returns 201 created" $         post "/simple_pk" [json| { "k":"k1", "extra":"e1" } |] `shouldRespondWith` 201@@ -251,6 +275,20 @@           , matchHeaders = []           } +      it "successfully inserts a row with all-default columns with prefer=rep" $+        request methodPost "/items" [("Prefer", "return=representation")] "{}"+          `shouldRespondWith` [json|[{ id: 20 }]|]+          { matchStatus  = 201,+            matchHeaders = []+          }++      it "successfully inserts a row with all-default columns with prefer=rep and &select=" $+        request methodPost "/items?select=id" [("Prefer", "return=representation")] "{}"+          `shouldRespondWith` [json|[{ id: 21 }]|]+          { matchStatus  = 201,+            matchHeaders = []+          }+     context "POST with ?columns parameter" $ do       it "ignores json keys not included in ?columns" $ do         request methodPost "/articles?columns=id,body" [("Prefer", "return=representation")]@@ -383,6 +421,24 @@             matchHeaders = []           } +    context "with invalid json payload" $+      it "fails with 400 and error" $+        request methodPatch "/simple_pk" [] "}{ x = 2"+          `shouldRespondWith`+          [json|{"message":"Error in $: Failed reading: not a valid json value at '}{x=2'"}|]+          { matchStatus  = 400,+            matchHeaders = [matchContentTypeJson]+          }++    context "with no payload" $+      it "fails with 400 and error" $+        request methodPatch "/items" [] ""+          `shouldRespondWith`+          [json|{"message":"Error in $: not enough input"}|]+          { matchStatus  = 400,+            matchHeaders = [matchContentTypeJson]+          }+     context "in a nonempty table" $ do       it "can update a single item" $ do         g <- get "/items?id=eq.42"@@ -464,48 +520,124 @@               matchHeaders = []             } -      it "can provide a representation" $ do-        _ <- post "/items"-          [json| { id: 1 } |]-        request methodPatch-          "/items?id=eq.1"-          [("Prefer", "return=representation")]-          [json| { id: 99 } |]-          `shouldRespondWith` [json| [{id:99}] |]-          { matchHeaders = [matchContentTypeJson] }-        -- put value back for other tests-        void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]+      context "with representation requested" $ do+        it "can provide a representation" $ do+          _ <- post "/items"+            [json| { id: 1 } |]+          request methodPatch+            "/items?id=eq.1"+            [("Prefer", "return=representation")]+            [json| { id: 99 } |]+            `shouldRespondWith` [json| [{id:99}] |]+            { matchHeaders = [matchContentTypeJson] }+          -- put value back for other tests+          void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |] -      it "makes no updates and returns 204, when patching with an empty json object/array" $ do-        request methodPatch "/items" [] [json| {} |]-          `shouldRespondWith` ""-          {-            matchStatus  = 204,-            matchHeaders = ["Content-Range" <:> "*/*"]-          }+        it "can return computed columns" $+          request methodPatch+            "/items?id=eq.1&select=id,always_true"+            [("Prefer", "return=representation")]+            [json| { id: 1 } |]+            `shouldRespondWith` [json| [{ id: 1, always_true: true }] |]+            { matchHeaders = [matchContentTypeJson] } -        request methodPatch "/items" [] [json| [] |]+        it "can select overloaded computed columns" $ do+          request methodPatch+            "/items?id=eq.1&select=id,computed_overload"+            [("Prefer", "return=representation")]+            [json| { id: 1 } |]+            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]+            { matchHeaders = [matchContentTypeJson] }+          request methodPatch+            "/items2?id=eq.1&select=id,computed_overload"+            [("Prefer", "return=representation")]+            [json| { id: 1 } |]+            `shouldRespondWith` [json| [{ id: 1, computed_overload: true }] |]+            { matchHeaders = [matchContentTypeJson] }++      it "ignores ?select= when return not set or return=minimal" $ do+        request methodPatch "/items?id=eq.1&select=id" [] [json| { id:1 } |]           `shouldRespondWith` ""           {             matchStatus  = 204,-            matchHeaders = ["Content-Range" <:> "*/*"]+            matchHeaders = ["Content-Range" <:> "0-0/*"]           }--        request methodPatch "/items" [] [json| [{}] |]+        request methodPatch "/items?id=eq.1&select=id" [("Prefer", "return=minimal")] [json| { id:1 } |]           `shouldRespondWith` ""           {             matchStatus  = 204,-            matchHeaders = ["Content-Range" <:> "*/*"]+            matchHeaders = ["Content-Range" <:> "0-0/*"]           } -      it "makes no updates and returns 200, when patching with an empty json object and return=rep" $-        request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]-          `shouldRespondWith` "[]"-          {-            matchStatus  = 200,-            matchHeaders = ["Content-Range" <:> "*/*"]-          }+      context "when patching with an empty body" $ do+        it "makes no updates and returns 204 without return= and without ?select=" $ do+          request methodPatch "/items" [] [json| {} |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            } +          request methodPatch "/items" [] [json| [] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++          request methodPatch "/items" [] [json| [{}] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 204 without return= and with ?select=" $ do+          request methodPatch "/items?select=id" [] [json| {} |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++          request methodPatch "/items?select=id" [] [json| [] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++          request methodPatch "/items?select=id" [] [json| [{}] |]+            `shouldRespondWith` ""+            {+              matchStatus  = 204,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 200 with return=rep and without ?select=" $+          request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]+            `shouldRespondWith` "[]"+            {+              matchStatus  = 200,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 200 with return=rep and with ?select=" $+          request methodPatch "/items?select=id" [("Prefer", "return=representation")] [json| {} |]+            `shouldRespondWith` "[]"+            {+              matchStatus  = 200,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }++        it "makes no updates and returns 200 with return=rep and with ?select= for overloaded computed columns" $+          request methodPatch "/items?select=id,computed_overload" [("Prefer", "return=representation")] [json| {} |]+            `shouldRespondWith` "[]"+            {+              matchStatus  = 200,+              matchHeaders = ["Content-Range" <:> "*/*"]+            }+     context "with unicode values" $       it "succeeds and returns values intact" $ do         void $ request methodPost "/no_pk" []@@ -561,7 +693,7 @@         , matchHeaders = [ matchContentTypeJson , "Location" <:> "/web_content?id=eq.6" ]         } -    it "embeds childs after update" $+    it "embeds children after update" $       request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name)"               [("Prefer", "return=representation")]         [json|{"name": "tardis-patched"}|]@@ -573,7 +705,7 @@           matchHeaders = [matchContentTypeJson]         } -    it "embeds parent, childs and grandchilds after update" $+    it "embeds parent, children and grandchildren after update" $       request methodPatch "/web_content?id=eq.0&select=id,name,web_content(name,web_content(name)),parent_content:p_web_id(name)"               [("Prefer", "return=representation")]         [json|{"name": "tardis-patched-2"}|]@@ -594,7 +726,7 @@           matchHeaders = [matchContentTypeJson]         } -    it "embeds childs after update without explicitly including the id in the ?select" $+    it "embeds children after update without explicitly including the id in the ?select" $       request methodPatch "/web_content?id=eq.0&select=name,web_content(name)"               [("Prefer", "return=representation")]         [json|{"name": "tardis-patched"}|]
test/Feature/MultipleSchemaSpec.hs view
@@ -77,7 +77,7 @@      context "Inserting tables on different schemas" $ do       it "succeeds inserting on default schema and returning it" $-        request methodPost "/childs" [("Prefer", "return=representation")] [json|{"name": "child v1-1", "parent_id": 1}|]+        request methodPost "/children" [("Prefer", "return=representation")] [json|{"name": "child v1-1", "parent_id": 1}|]          `shouldRespondWith`          [json|[{"id":1, "name": "child v1-1", "parent_id": 1}]|]          {@@ -86,7 +86,7 @@          }        it "succeeds inserting on the v1 schema and returning its parent" $-        request methodPost "/childs?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v1")]+        request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v1")]           [json|{"name": "child v1-2", "parent_id": 2}|]           `shouldRespondWith`           [json|[{"id":2, "parent": {"id": 2, "name": "parent v1-2"}}]|]@@ -96,7 +96,7 @@           }        it "succeeds inserting on the v2 schema and returning its parent" $-        request methodPost "/childs?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v2")]+        request methodPost "/children?select=id,parent(*)" [("Prefer", "return=representation"), ("Content-Profile", "v2")]           [json|{"name": "child v2-3", "parent_id": 3}|]           `shouldRespondWith`           [json|[{"id":1, "parent": {"id": 3, "name": "parent v2-3"}}]|]@@ -106,7 +106,7 @@           }        it "fails when inserting on an unknown schema" $-        request methodPost "/childs" [("Content-Profile", "unknown")]+        request methodPost "/children" [("Content-Profile", "unknown")]           [json|{"name": "child 4", "parent_id": 4}|]           `shouldRespondWith`           [json|{"message":"The schema must be one of the following: v1, v2"}|]@@ -125,30 +125,42 @@           }        it "succeeds in calling the v1 schema proc and embedding" $-        request methodGet "/rpc/get_parents_below?id=6&select=id,name,childs(id,name)" [("Accept-Profile", "v1")] ""+        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v1")] ""           `shouldRespondWith`           [json| [-            {"id":1,"name":"parent v1-1","childs":[{"id":1,"name":"child v1-1"}]},-            {"id":2,"name":"parent v1-2","childs":[{"id":2,"name":"child v1-2"}]}] |]+            {"id":1,"name":"parent v1-1","children":[{"id":1,"name":"child v1-1"}]},+            {"id":2,"name":"parent v1-2","children":[{"id":2,"name":"child v1-2"}]}] |]           {             matchStatus = 200           , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v1"]           }        it "succeeds in calling the v2 schema proc and embedding" $-        request methodGet "/rpc/get_parents_below?id=6&select=id,name,childs(id,name)" [("Accept-Profile", "v2")] ""+        request methodGet "/rpc/get_parents_below?id=6&select=id,name,children(id,name)" [("Accept-Profile", "v2")] ""           `shouldRespondWith`           [json| [-            {"id":3,"name":"parent v2-3","childs":[{"id":1,"name":"child v2-3"}]},-            {"id":4,"name":"parent v2-4","childs":[]}] |]+            {"id":3,"name":"parent v2-3","children":[{"id":1,"name":"child v2-3"}]},+            {"id":4,"name":"parent v2-4","children":[]}] |]           {             matchStatus = 200           , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]           } +      it "succeeds in calling the v2 schema proc with POST by using Content-Profile" $+        request methodPost "/rpc/get_parents_below?select=id,name" [("Content-Profile", "v2")]+          [json|{"id": "6"}|]+          `shouldRespondWith`+          [json| [+            {"id":3,"name":"parent v2-3"},+            {"id":4,"name":"parent v2-4"}]|]+          {+            matchStatus = 200+          , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]+          }+     context "Modifying tables on different schemas" $ do       it "succeeds in patching on the v1 schema and returning its parent" $-        request methodPatch "/childs?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]+        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v1"), ("Prefer", "return=representation")]           [json|{"name": "child v1-1 updated"}|]           `shouldRespondWith`           [json|[{"name":"child v1-1 updated", "parent": {"name": "parent v1-1"}}]|]@@ -158,7 +170,7 @@           }        it "succeeds in patching on the v2 schema and returning its parent" $-        request methodPatch "/childs?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]+        request methodPatch "/children?select=name,parent(name)&id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")]           [json|{"name": "child v2-1 updated"}|]           `shouldRespondWith`           [json|[{"name":"child v2-1 updated", "parent": {"name": "parent v2-3"}}]|]@@ -168,13 +180,13 @@           }        it "succeeds on deleting on the v2 schema" $ do-        request methodDelete "/childs?id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")] ""+        request methodDelete "/children?id=eq.1" [("Content-Profile", "v2"), ("Prefer", "return=representation")] ""           `shouldRespondWith` [json|[{"id": 1, "name": "child v2-1 updated", "parent_id": 3}]|]           {             matchStatus = 200           , matchHeaders = [matchContentTypeJson, "Content-Profile" <:> "v2"]           }-        request methodGet "/childs?id=eq.1" [("Accept-Profile", "v2")] ""+        request methodGet "/children?id=eq.1" [("Accept-Profile", "v2")] ""           `shouldRespondWith` "[]"           {             matchStatus = 200@@ -183,7 +195,7 @@        when (actualPgVersion >= pgVersion96) $         it "succeeds on PUT on the v2 schema" $-          request methodPut "/childs?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]+          request methodPut "/children?id=eq.111" [("Content-Profile", "v2"), ("Prefer", "return=representation")]             [json| [ { "id": 111, "name": "child v2-111", "parent_id": null } ]|]             `shouldRespondWith`             [json|[{ "id": 111, "name": "child v2-111", "parent_id": null }]|]
test/Feature/QuerySpec.hs view
@@ -237,7 +237,6 @@         [json| [{"myId":1}] |]         { matchHeaders = [matchContentTypeJson] } -     it "one simple column with casting (text)" $       get "/complex_items?select=id::text" `shouldRespondWith`         [json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]@@ -327,6 +326,30 @@         [json|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]         { matchHeaders = [matchContentTypeJson] } +    describe "computed columns" $ do+      it "computed column on table" $+        get "/items?id=eq.1&select=id,always_true" `shouldRespondWith`+          [json|[{"id":1,"always_true":true}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "computed column on rpc" $+        get "/rpc/search?id=1&select=id,always_true" `shouldRespondWith`+          [json|[{"id":1,"always_true":true}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "overloaded computed columns on both tables" $ do+        get "/items?id=eq.1&select=id,computed_overload" `shouldRespondWith`+          [json|[{"id":1,"computed_overload":true}]|]+          { matchHeaders = [matchContentTypeJson] }+        get "/items2?id=eq.1&select=id,computed_overload" `shouldRespondWith`+          [json|[{"id":1,"computed_overload":true}]|]+          { matchHeaders = [matchContentTypeJson] }++      it "overloaded computed column on rpc" $+        get "/rpc/search?id=1&select=id,computed_overload" `shouldRespondWith`+          [json|[{"id":1,"computed_overload":true}]|]+          { matchHeaders = [matchContentTypeJson] }+     describe "view embedding" $ do       it "can detect fk relations through views to tables in the public schema" $         get "/consumers_view?select=*,orders_view(*)" `shouldRespondWith` 200@@ -482,18 +505,18 @@               "designTasks":[ ] }           ]|] { matchHeaders = [matchContentTypeJson] } -      it "works with two aliased childs embeds plus and/or" $-        get "/entities?select=id,childs:child_entities(id,gChilds:grandchild_entities(id))&childs.and=(id.in.(1,2,3))&childs.gChilds.or=(id.eq.1,id.eq.2)" `shouldRespondWith`+      it "works with two aliased children embeds plus and/or" $+        get "/entities?select=id,children:child_entities(id,gChildren:grandchild_entities(id))&children.and=(id.in.(1,2,3))&children.gChildren.or=(id.eq.1,id.eq.2)" `shouldRespondWith`           [json|[             { "id":1,-              "childs":[-                {"id":1,"gChilds":[{"id":1}, {"id":2}]},-                {"id":2,"gChilds":[]}]},+              "children":[+                {"id":1,"gChildren":[{"id":1}, {"id":2}]},+                {"id":2,"gChildren":[]}]},             { "id":2,-              "childs":[-                {"id":3,"gChilds":[]}]},-            { "id":3,"childs":[]},-            { "id":4,"childs":[]}+              "children":[+                {"id":3,"gChildren":[]}]},+            { "id":3,"children":[]},+            { "id":4,"children":[]}           ]|] { matchHeaders = [matchContentTypeJson] }    describe "ordering response" $ do
test/Feature/UnicodeSpec.hs view
@@ -1,7 +1,6 @@ module Feature.UnicodeSpec where -import Control.Monad (void)-import Network.Wai   (Application)+import Network.Wai (Application)  import Test.Hspec import Test.Hspec.Wai
test/Feature/UpsertSpec.hs view
@@ -180,26 +180,6 @@             [json|{"message":"Range header and limit/offset querystring parameters are not allowed for PUT"}|]             { matchStatus = 400 , matchHeaders = [matchContentTypeJson] } -        it "fails if the payload has more than one row" $-          put "/tiobe_pls?name=eq.Go"-            [str| [ { "name": "Go", "rank": 19 }, { "name": "Swift", "rank": 12 } ]|]-            `shouldRespondWith`-            [json|{"message":"PUT payload must contain a single row"}|]-            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }--        it "fails if not all columns are specified" $ do-          put "/tiobe_pls?name=eq.Go"-            [str| [ { "name": "Go" } ]|]-            `shouldRespondWith`-            [json|{"message":"You must specify all columns in the payload when using PUT"}|]-            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }--          put "/employees?first_name=eq.Susan&last_name=eq.Heidt"-            [str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000" } ]|]-            `shouldRespondWith`-            [json|{"message":"You must specify all columns in the payload when using PUT"}|]-            { matchStatus = 400 , matchHeaders = [matchContentTypeJson] }-         it "rejects every other filter than pk cols eq's" $ do           put "/tiobe_pls?rank=eq.19"             [str| [ { "name": "Go", "rank": 19 } ]|]@@ -279,6 +259,14 @@           get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json|[ { "name": "Go", "rank": 19 } ]|] { matchHeaders = [matchContentTypeJson] }           put "/tiobe_pls?name=eq.Go" [str| [ { "name": "Go", "rank": 13 } ]|] `shouldRespondWith` 204           get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 13 } ]|] { matchHeaders = [matchContentTypeJson] }++        it "succeeds if the payload has more than one row, but it only puts the first element" $+          request methodPut "/tiobe_pls?name=eq.Go"+            [("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]+            [str| [ { "name": "Go", "rank": 19 }, { "name": "Swift", "rank": 12 } ] |]+            `shouldRespondWith`+            [json|{ "name": "Go", "rank": 19 }|]+            { matchStatus = 200 , matchHeaders = [matchContentTypeSingular] }          it "succeeds on table with composite pk" $ do           get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
test/Main.hs view
@@ -16,7 +16,8 @@ import PostgREST.Config      (AppConfig (..)) import PostgREST.DbStructure (getDbStructure, getPgVersion) import PostgREST.Types       (pgVersion95, pgVersion96)-import Protolude             hiding (toList)+import Protolude             hiding (toList, toS)+import Protolude.Conv        (toS) import SpecHelper  import qualified Feature.AndOrParamsSpec@@ -55,7 +56,6 @@   getTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }    testDbConn <- getEnvVarWithDefault "POSTGREST_TEST_CONNECTION" "postgres://postgrest_test@localhost/postgrest_test"-  setupDb testDbConn    pool <- P.acquire (3, 10, toS testDbConn) 
test/QueryCost.hs view
@@ -10,7 +10,8 @@ import qualified Hasql.Transaction.Sessions as HT import           Text.Heredoc -import Protolude hiding (get)+import Protolude      hiding (get, toS)+import Protolude.Conv (toS)  import PostgREST.QueryBuilder (requestToCallProcQuery) import PostgREST.Types@@ -22,40 +23,39 @@ main :: IO () main = do   testDbConn <- getEnvVarWithDefault "POSTGREST_TEST_CONNECTION" "postgres://postgrest_test@localhost/postgrest_test"-  -- To speed things up, assume setupDb has ben ran in the previous spec.   pool <- P.acquire (3, 10, toS testDbConn)    hspec $ describe "QueryCost" $     context "call proc query" $ do       it "should not exceed cost when calling setof composite proc" $ do         cost <- exec pool [str| {"id": 3} |] $-          requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False Nothing+          requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False Nothing []         liftIO $           cost `shouldSatisfy` (< Just 40)        it "should not exceed cost when calling setof composite proc with empty params" $ do         cost <- exec pool mempty $-          requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing+          requestToCallProcQuery (QualifiedIdentifier "test" "getallprojects") [] False Nothing []         liftIO $           cost `shouldSatisfy` (< Just 30)        it "should not exceed cost when calling scalar proc" $ do         cost <- exec pool [str| {"a": 3, "b": 4} |] $-          requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing+          requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing []         liftIO $           cost `shouldSatisfy` (< Just 10)        context "params=multiple-objects" $ do         it "should not exceed cost when calling setof composite proc" $ do           cost <- exec pool [str| [{"id": 1}, {"id": 4}] |] $-            requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False (Just MultipleObjects)+            requestToCallProcQuery (QualifiedIdentifier "test" "get_projects_below") [PgArg "id" "int" True] False (Just MultipleObjects) []           liftIO $ do             cost `shouldSatisfy` (> Just 2000)             cost `shouldSatisfy` (< Just 2100)          it "should not exceed cost when calling scalar proc" $ do           cost <- exec pool [str| [{"a": 3, "b": 4}, {"a": 1, "b": 2}, {"a": 8, "b": 7}] |] $-            requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing+            requestToCallProcQuery (QualifiedIdentifier "test" "add_them") [PgArg "a" "int" True, PgArg "b" "int" True] True Nothing []           liftIO $             cost `shouldSatisfy` (< Just 10) 
test/SpecHelper.hs view
@@ -7,7 +7,6 @@ import qualified Data.Set               as S import qualified System.IO.Error        as E -import Control.Monad        (void) import Data.Aeson           (Value (..), decode, encode) import Data.CaseInsensitive (CI (..)) import Data.List            (lookup)@@ -25,7 +24,8 @@  import PostgREST.Config (AppConfig (..)) import PostgREST.Types  (JSPathExp (..))-import Protolude+import Protolude        hiding (toS)+import Protolude.Conv   (toS)  matchContentTypeJson :: MatchHeader matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"@@ -144,16 +144,6 @@  testMultipleSchemaCfg :: Text -> AppConfig testMultipleSchemaCfg testDbConn = (testCfg testDbConn) { configSchemas = fromList ["v1", "v2"] }--setupDb :: Text -> IO ()-setupDb dbConn = do-  loadFixture dbConn "database"-  loadFixture dbConn "roles"-  loadFixture dbConn "schema"-  loadFixture dbConn "jwt"-  loadFixture dbConn "jsonschema"-  loadFixture dbConn "privileges"-  resetDb dbConn  resetDb :: Text -> IO () resetDb dbConn = loadFixture dbConn "data"