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 +10/−0
- main/Main.hs +3/−3
- postgrest.cabal +46/−28
- src/PostgREST/ApiRequest.hs +11/−11
- src/PostgREST/App.hs +16/−20
- src/PostgREST/Auth.hs +2/−1
- src/PostgREST/Config.hs +6/−8
- src/PostgREST/DbRequestBuilder.hs +4/−3
- src/PostgREST/DbStructure.hs +113/−170
- src/PostgREST/Error.hs +12/−20
- src/PostgREST/Middleware.hs +2/−1
- src/PostgREST/OpenAPI.hs +4/−3
- src/PostgREST/Parsers.hs +3/−3
- src/PostgREST/Private/QueryFragment.hs +5/−4
- src/PostgREST/QueryBuilder.hs +21/−9
- src/PostgREST/RangeQuery.hs +4/−3
- src/PostgREST/Statements.hs +8/−4
- src/PostgREST/Types.hs +4/−4
- test/Feature/ConcurrentSpec.hs +0/−1
- test/Feature/DeleteSpec.hs +15/−0
- test/Feature/EmbedDisambiguationSpec.hs +9/−9
- test/Feature/InsertSpec.hs +167/−35
- test/Feature/MultipleSchemaSpec.hs +27/−15
- test/Feature/QuerySpec.hs +33/−10
- test/Feature/UnicodeSpec.hs +1/−2
- test/Feature/UpsertSpec.hs +8/−20
- test/Main.hs +2/−2
- test/QueryCost.hs +7/−7
- test/SpecHelper.hs +2/−12
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"