bugzilla-redhat 0.3.1 → 0.3.2
raw patch · 11 files changed
+226/−53 lines, 11 filesdep +hspecdep ~basePVP: major bump suggested
API removals or changes: PVP suggests a major version bump
Dependencies added: hspec
Dependency ranges changed: base
API changes (from Hackage documentation)
+ Web.Bugzilla.RedHat: ApikeySession :: BugzillaContext -> BugzillaApikey -> BugzillaSession
+ Web.Bugzilla.RedHat: BugzillaApikey :: Text -> BugzillaApikey
+ Web.Bugzilla.RedHat: ExternalBug :: Text -> Int -> Text -> Text -> Text -> Int -> ExternalType -> ExternalBug
+ Web.Bugzilla.RedHat: ExternalType :: Text -> Text -> Int -> Text -> Text -> ExternalType
+ Web.Bugzilla.RedHat: [bugExternalBugs] :: Bug -> Maybe [ExternalBug]
+ Web.Bugzilla.RedHat: [externalBugId] :: ExternalBug -> Text
+ Web.Bugzilla.RedHat: [externalBzId] :: ExternalBug -> Int
+ Web.Bugzilla.RedHat: [externalDescription] :: ExternalBug -> Text
+ Web.Bugzilla.RedHat: [externalId] :: ExternalBug -> Int
+ Web.Bugzilla.RedHat: [externalPriority] :: ExternalBug -> Text
+ Web.Bugzilla.RedHat: [externalStatus] :: ExternalBug -> Text
+ Web.Bugzilla.RedHat: [externalTypeDescription] :: ExternalType -> Text
+ Web.Bugzilla.RedHat: [externalTypeFullUrl] :: ExternalType -> Text
+ Web.Bugzilla.RedHat: [externalTypeId] :: ExternalType -> Int
+ Web.Bugzilla.RedHat: [externalTypeType] :: ExternalType -> Text
+ Web.Bugzilla.RedHat: [externalTypeUrl] :: ExternalType -> Text
+ Web.Bugzilla.RedHat: [externalType] :: ExternalBug -> ExternalType
+ Web.Bugzilla.RedHat: apikeySession :: BugzillaContext -> BugzillaApikey -> BugzillaSession
+ Web.Bugzilla.RedHat: data ExternalBug
+ Web.Bugzilla.RedHat: data ExternalType
+ Web.Bugzilla.RedHat: data Request
+ Web.Bugzilla.RedHat: getBugAll :: BugzillaSession -> BugId -> IO (Maybe Bug)
+ Web.Bugzilla.RedHat: newtype BugzillaApikey
+ Web.Bugzilla.RedHat: searchBugsAll :: BugzillaSession -> SearchExpression -> IO [Bug]
+ Web.Bugzilla.RedHat: searchBugsAllWithLimit :: BugzillaSession -> Int -> Int -> SearchExpression -> IO [Bug]
+ Web.Bugzilla.RedHat.Search: changedRange :: UTCTime -> UTCTime -> SearchExpression
+ Web.Bugzilla.RedHat.Search: changedSince :: UTCTime -> SearchExpression
+ Web.Bugzilla.RedHat.Search: changedUntil :: UTCTime -> SearchExpression
+ Web.Bugzilla.RedHat.Search: isNotEmpty :: FieldType a => Field a -> SearchExpression
- Web.Bugzilla.RedHat: Bug :: !BugId -> Maybe [Text] -> UserEmail -> User -> [BugId] -> [UserEmail] -> [User] -> Text -> [Text] -> UTCTime -> UserEmail -> User -> [BugId] -> Maybe BugId -> Maybe [Flag] -> [Text] -> Bool -> Bool -> Bool -> Bool -> [Text] -> UTCTime -> Text -> Text -> Text -> Text -> UserEmail -> Text -> [Text] -> Text -> Text -> Text -> Text -> Text -> [Text] -> Text -> HashMap Text Text -> Bug
+ Web.Bugzilla.RedHat: Bug :: !BugId -> Maybe [Text] -> UserEmail -> User -> [BugId] -> [UserEmail] -> [User] -> Text -> [Text] -> UTCTime -> UserEmail -> User -> [BugId] -> Maybe BugId -> Maybe [Flag] -> [Text] -> Bool -> Bool -> Bool -> Bool -> [Text] -> UTCTime -> Text -> Text -> Text -> Text -> UserEmail -> Text -> [Text] -> Text -> Text -> Text -> Text -> Text -> [Text] -> Text -> HashMap Text Text -> Maybe [ExternalBug] -> Bug
Files
- ChangeLog.md +11/−0
- README.md +9/−0
- TODO +7/−0
- bugzilla-redhat.cabal +19/−7
- changelog.orig +0/−18
- src/Web/Bugzilla/RedHat.hs +60/−21
- src/Web/Bugzilla/RedHat/Internal/Network.hs +19/−7
- src/Web/Bugzilla/RedHat/Internal/Search.hs +2/−0
- src/Web/Bugzilla/RedHat/Internal/Types.hs +44/−0
- src/Web/Bugzilla/RedHat/Search.hs +19/−0
- test/Spec.hs +36/−0
ChangeLog.md view
@@ -1,5 +1,16 @@ # bugzilla-redhat version history +## 0.3.2 (2021-06-19)+- BugillaServer can now be fully qualified (@TristanCacqueray)+- Add apikeySession to support api_key auth (@TristanCacqueray)+- Add isNotEmpty search expression operator (@TristanCacqueray)+- Add changedSince and changedUntil search expression (@TristanCacqueray)+- Add searchBugsAllWithLimit, searchBugsAll and getBugAll+ to get all the bug fields (@TristanCacqueray)+- Change Bug to include ExternalBugs information (@TristanCacqueray)+- Export Request+- support ghc-9.0 (@juhp)+ ## 0.3.1 (2021-02-07) - export sendBzRequest
README.md view
@@ -10,3 +10,12 @@ Relevant links: - https://bugzilla.redhat.com/docs/en/html/api/index.html++## Contributing+Bug reports, feedback, and fixes are welcome.++Thanks to the contributors!++<a href="https://github.com/juhp/hsbugzilla/graphs/contributors">+ <img src="https://contributors-img.web.app/image?repo=juhp/hsbugzilla" />+</a>
+ TODO view
@@ -0,0 +1,7 @@+- support getting bug's requested fields++- instance Monoid SearchExpression?++- rename Bugzilla.Redhat to RedHatBugzilla for 0.4++- use http-query (http-conduit Simple)
bugzilla-redhat.cabal view
@@ -1,8 +1,8 @@ name: bugzilla-redhat-version: 0.3.1+version: 0.3.2 synopsis: A Haskell interface to the Bugzilla native REST API-description: This package is designed to provide an easy-to-use, typesafe- interface to querying Bugzilla from Haskell.+description: This package is designed to provide an easy-to-use,+ type-safe interface to querying Bugzilla from Haskell. . This is a friendly fork of Seth Fowler's library, with minor updates and API tweaks needed for@@ -13,12 +13,12 @@ author: Seth Fowler <mark.seth.fowler@gmail.com> maintainer: Jens Petersen <juhpetersen@gmail.com> copyright: 2014 Seth Fowler,- 2020 Jens Petersen+ 2020-2021 Jens Petersen category: Web build-type: Simple extra-source-files: README.md ChangeLog.md- changelog.orig+ TODO cabal-version: >=1.10 source-repository head@@ -36,7 +36,7 @@ other-modules: Web.Bugzilla.RedHat.Internal.Network, Web.Bugzilla.RedHat.Internal.Search, Web.Bugzilla.RedHat.Internal.Types- build-depends: base >=4.6 && <4.15,+ build-depends: base >=4.6 && <5, aeson >=0.7 && <1.6, blaze-builder >=0.3 && <0.5, bytestring >=0.10 && <0.11,@@ -62,10 +62,22 @@ ghc-options: -Wall main-is: BugzillaDemo.hs- build-depends: base >=4.6 && <4.15,+ build-depends: base >=4.6 && <5, bugzilla-redhat, containers >=0.5 && <0.7, text >=0.11 && <1.3, time >=1.4 && <1.10 hs-source-dirs: demo+ default-language: Haskell2010++test-suite bugzilla-test+ type: exitcode-stdio-1.0+ hs-source-dirs: test+ main-is: Spec.hs+ build-depends: base >=4.6 && <5,+ aeson >=0.7 && <1.6,+ bugzilla-redhat,+ hspec >=2.0 && <3.0,+ time >=1.4 && <1.10+ ghc-options: -Wall default-language: Haskell2010
− changelog.orig
@@ -1,18 +0,0 @@-0.2.1.1-- * Tightened the version bounds for data-default. The old bounds weren't- accurate.--0.2.1.0-- * Added searchBugs' and searchBugsWithLimit', variations which return- only a list of BugIds. These are more suitable for polling than the- existing functions.--0.2.0.0-- * History events now have a sequential id, just like comments.-- * When using an anonymous session, Bugzilla does not return email- addresses for users. To accomodate this, the User type now stores email- addresses as type 'Maybe UserEmail'.
src/Web/Bugzilla/RedHat.hs view
@@ -10,7 +10,7 @@ -- -- A very simple program using this package might look like this: ----- > ctx <- newBugzillaContext "bugzilla.example.org"+-- > ctx <- newBugzillaContext "bugzilla.redhat.com" -- > let session = anonymousSession ctx -- > user = "me@example.org" -- > query = AssignedToField .==. user .&&.@@ -21,23 +21,29 @@ -- -- There's a somewhat more in-depth demo program included with the -- source code to this package.+ module Web.Bugzilla.RedHat ( -- * Connecting to Bugzilla newBugzillaContext , loginSession+, apikeySession , anonymousSession , BugzillaServer , BugzillaContext , BugzillaSession (..) , BugzillaToken (..)+, BugzillaApikey (..) -- * Querying Bugzilla , searchBugs+, searchBugsAll , searchBugs' , searchBugsWithLimit+, searchBugsAllWithLimit , searchBugsWithLimit' , getBug+, getBugAll , getAttachment , getAttachments , getComments@@ -49,6 +55,7 @@ , sendBzRequest , intAsText +, Request , BugId , AttachmentId , CommentId@@ -61,6 +68,8 @@ , User (..) , Flag (..) , Bug (..)+, ExternalBug (..)+, ExternalType (..) , Attachment (..) , Comment (..) , History (..)@@ -74,6 +83,7 @@ import Control.Exception (throw, try) import Control.Monad.IO.Class (liftIO)+import Data.Aeson (FromJSON) import qualified Data.Text as T import Network.Connection (TLSSettings(..)) import Network.HTTP.Conduit (mkManagerSettings, newManager)@@ -106,6 +116,10 @@ Left e -> throw e Right token -> Just $ LoginSession ctx token +-- | Creates a 'BugzillaSession' using the provided api key.+apikeySession :: BugzillaContext -> BugzillaApikey -> BugzillaSession+apikeySession = ApikeySession+ -- | Creates an anonymous 'BugzillaSession'. Note that some content -- will be hidden by Bugzilla when you make queries in this state. anonymousSession :: BugzillaContext -> BugzillaSession@@ -118,11 +132,15 @@ -- can be constructed conveniently using the operators in "Web.Bugzilla.Search". searchBugs :: BugzillaSession -> SearchExpression -> IO [Bug] searchBugs session search = do- let searchQuery = evalSearchExpr search- req = newBzRequest session ["bug"] searchQuery- (BugList bugs) <- sendBzRequest session req+ BugList bugs <- doSearchBugs session search Nothing Nothing return bugs +-- | Similar to 'searchBugs', but return _all fields.+searchBugsAll :: BugzillaSession -> SearchExpression -> IO [Bug]+searchBugsAll session search = do+ BugList bugs <- doSearchBugs session search (Just "_all") Nothing+ return bugs+ -- | Like 'searchBugs', but returns a list of 'BugId's. You can -- retrieve the 'Bug' for each 'BugId' using 'getBug'. The combination -- of 'searchBugs'' and 'getBug' is much less efficient than@@ -131,12 +149,22 @@ -- set of bugs returned by a query has changed. searchBugs' :: BugzillaSession -> SearchExpression -> IO [BugId] searchBugs' session search = do- let fieldsQuery = [("include_fields", Just "id")]- searchQuery = evalSearchExpr search- req = newBzRequest session ["bug"] (fieldsQuery ++ searchQuery)- (BugIdList bugs) <- sendBzRequest session req+ BugIdList bugs <- doSearchBugs session search (Just "id") Nothing return bugs +doSearchBugs :: FromJSON a => BugzillaSession -> SearchExpression -> Maybe T.Text -> Maybe (Int, Int) -> IO a+doSearchBugs session search includeField limits = do+ let fieldsQuery = case includeField of+ Nothing -> []+ Just field -> [("include_fields", Just field)]+ limitQuery = case limits of+ Nothing -> []+ Just (limit, offset) -> [("limit", Just $ intAsText limit),+ ("offset", Just $ intAsText offset)]+ searchQuery = evalSearchExpr search+ req = newBzRequest session ["bug"] (limitQuery ++ fieldsQuery ++ searchQuery)+ sendBzRequest session req+ -- | Search Bugzilla and returns a limited number of results. You can -- call this repeatedly and use 'offset' to retrieve the results of -- a large query incrementally. Note that most Bugzillas won't@@ -149,13 +177,19 @@ -> SearchExpression -> IO [Bug] searchBugsWithLimit session limit offset search = do- let limitQuery = [("limit", Just $ intAsText limit),- ("offset", Just $ intAsText offset)]- searchQuery = evalSearchExpr search- req = newBzRequest session ["bug"] (limitQuery ++ searchQuery)- (BugList bugs) <- sendBzRequest session req+ BugList bugs <- doSearchBugs session search Nothing (Just (limit, offset)) return bugs +-- | Similar to 'searchBugsWithLimit', but return _all fields.+searchBugsAllWithLimit :: BugzillaSession+ -> Int -- ^ The maximum number of results to return.+ -> Int -- ^ The offset from the first result to start from.+ -> SearchExpression+ -> IO [Bug]+searchBugsAllWithLimit session limit offset search = do+ BugList bugs <- doSearchBugs session search (Just "_all") (Just (limit, offset))+ return bugs+ -- | Like 'searchBugsWithLimit', but returns a list of 'BugId's. See -- 'searchBugs'' for more discussion. searchBugsWithLimit' :: BugzillaSession@@ -164,24 +198,29 @@ -> SearchExpression -> IO [BugId] searchBugsWithLimit' session limit offset search = do- let fieldsQuery = [("include_fields", Just "id")]- limitQuery = [("limit", Just $ intAsText limit),- ("offset", Just $ intAsText offset)]- searchQuery = evalSearchExpr search- req = newBzRequest session ["bug"] (fieldsQuery ++ limitQuery ++ searchQuery)- (BugIdList bugs) <- sendBzRequest session req+ BugIdList bugs <- doSearchBugs session search (Just "id") (Just (limit, offset)) return bugs -- | Retrieve a bug by bug number. getBug :: BugzillaSession -> BugId -> IO (Maybe Bug)-getBug session bid = do- let req = newBzRequest session ["bug", intAsText bid] []+getBug session bid = getBugIncludeFields session bid []++-- | Retrieve all bug field by bug number+getBugAll :: BugzillaSession -> BugId -> IO (Maybe Bug)+getBugAll session bid = getBugIncludeFields session bid ["_all"]++-- | Retrieve a bug by bug number with fields+getBugIncludeFields :: BugzillaSession -> BugId -> [T.Text] -> IO (Maybe Bug)+getBugIncludeFields session bid includeFields = do+ let req = newBzRequest session ["bug", intAsText bid] query (BugList bugs) <- sendBzRequest session req case bugs of [bug] -> return $ Just bug [] -> return Nothing _ -> throw $ BugzillaUnexpectedValue "Request for a single bug returned multiple bugs"+ where+ query = map (\f -> ("include_fields", Just f)) includeFields -- | Retrieve a bug by attachment number. getAttachment :: BugzillaSession -> AttachmentId -> IO (Maybe Attachment)
src/Web/Bugzilla/RedHat/Internal/Network.hs view
@@ -5,6 +5,7 @@ module Web.Bugzilla.RedHat.Internal.Network ( BugzillaServer , BugzillaContext (..)+, BugzillaApikey (..) , BugzillaToken (..) , BugzillaSession (..) , BugzillaException (..)@@ -24,6 +25,7 @@ import Control.Monad.IO.Class (liftIO) import Control.Monad.Trans.Resource (runResourceT) import Data.Aeson+import Data.Maybe (fromMaybe) import qualified Data.ByteString as B import qualified Data.ByteString.Lazy as BL #if !MIN_VERSION_base(4,11,0)@@ -32,14 +34,13 @@ import qualified Data.Text as T import qualified Data.Text.Encoding as TE import Data.Typeable-import Network.HTTP.Conduit (Manager, Request(..), Response(..), defaultRequest, host, httpLbs, path, queryString, secure)+import Network.HTTP.Conduit (Manager, Request(..), Response(..), defaultRequest, host, httpLbs, path, queryString, secure, parseRequest) import Network.HTTP.Types.URI (QueryText, encodePathSegments, renderQueryText) type BugzillaServer = T.Text -- | Holds information about a 'BugzillaServer' and manages outgoing--- connections. You can use 'newBugzillaContext' or--- 'withBugzillaContext' to create one.+-- connections. You can use 'newBugzillaContext' to create one. data BugzillaContext = BugzillaContext { bzServer :: BugzillaServer , bzManager :: Manager@@ -47,6 +48,8 @@ newtype BugzillaToken = BugzillaToken T.Text +newtype BugzillaApikey = BugzillaApikey T.Text+ instance FromJSON BugzillaToken where parseJSON (Object v) = BugzillaToken <$> v .: "token" parseJSON _ = mzero@@ -55,10 +58,12 @@ -- 'loginSession', as appropriate, to create one. data BugzillaSession = AnonymousSession BugzillaContext | LoginSession BugzillaContext BugzillaToken+ | ApikeySession BugzillaContext BugzillaApikey bzContext :: BugzillaSession -> BugzillaContext bzContext (AnonymousSession ctx) = ctx bzContext (LoginSession ctx _) = ctx+bzContext (ApikeySession ctx _) = ctx data BugzillaException = BugzillaJSONParseError String@@ -81,15 +86,22 @@ newBzRequest :: BugzillaSession -> [T.Text] -> QueryText -> Request newBzRequest session methodParts query =- sslRequest {- host = TE.encodeUtf8 . bzServer . bzContext $ session,+ baseRequest { path = toByteString $ encodePathSegments $ "rest" : methodParts, queryString = toByteString $ renderQueryText True queryWithToken } where+ -- Try to parse the bzServer first, if it has a scheme then use it as the base request,+ -- otherwise force a secure ssl request.+ baseRequest :: Request+ baseRequest = fromMaybe (sslRequest { host = serverBytes }) (parseRequest serverStr)+ serverBytes = TE.encodeUtf8 serverTxt+ serverStr = T.unpack serverTxt+ serverTxt = bzServer . bzContext $ session queryWithToken = case session of- AnonymousSession _ -> query- LoginSession _ (BugzillaToken token) -> ("token", Just token) : query+ AnonymousSession _ -> query+ LoginSession _ (BugzillaToken token) -> ("token", Just token) : query+ ApikeySession _ (BugzillaApikey token) -> ("api_key", Just token) : query data BzError = BzError Int String deriving (Eq, Show)
src/Web/Bugzilla/RedHat/Internal/Search.hs view
@@ -37,6 +37,7 @@ data SearchTerm where UnaryOp :: FieldType a => T.Text -> Field a -> SearchTerm BinaryOp :: (FieldType a, FieldType b) => T.Text -> Field a -> b -> SearchTerm+ EqTerm :: (FieldType a, FieldType b) => Field a -> b -> SearchTerm -- | A Boolean expression which can be used to query Bugzilla. data SearchExpression@@ -56,6 +57,7 @@ evalSearchTerm :: Int -> SearchTerm -> [QueryPart] evalSearchTerm t (UnaryOp op field) = termQuery t field op ("" :: T.Text) evalSearchTerm t (BinaryOp op field val) = termQuery t field op val+evalSearchTerm _ (EqTerm field val) = [(searchFieldName field, Just . fvAsText $ val)] evalSearchExpr :: SearchExpression -> [QueryPart] evalSearchExpr e = snd $ evalSearchExpr' 1 e
src/Web/Bugzilla/RedHat/Internal/Types.hs view
@@ -18,6 +18,8 @@ , User (..) , UserList (..) , Flag (..)+, ExternalType (..)+, ExternalBug (..) , Bug (..) , BugList (..) , BugIdList (..)@@ -338,6 +340,46 @@ <*> v .:? "requestee" parseJSON _ = mzero +-- | An external bug type+data ExternalType = ExternalType+ { externalTypeDescription :: T.Text+ , externalTypeUrl :: T.Text+ , externalTypeId :: Int+ , externalTypeType :: T.Text+ , externalTypeFullUrl :: T.Text+ } deriving (Eq, Ord, Show)++instance FromJSON ExternalType where+ parseJSON (Object v) =+ ExternalType <$> v .: "description"+ <*> v .: "url"+ <*> v .: "id"+ <*> v .: "type"+ <*> v .: "full_url"+ parseJSON _ = mzero++-- | An external bug.+data ExternalBug = ExternalBug+ { externalDescription :: T.Text+ , externalBzId :: Int+ , externalPriority :: T.Text+ , externalBugId :: T.Text+ , externalStatus :: T.Text+ , externalId :: Int+ , externalType :: ExternalType+ } deriving (Eq, Ord, Show)++instance FromJSON ExternalBug where+ parseJSON (Object v) =+ ExternalBug <$> v .: "ext_description"+ <*> v .: "ext_bz_id"+ <*> v .: "ext_priority"+ <*> v .: "ext_bz_bug_id"+ <*> v .: "ext_status"+ <*> v .: "id"+ <*> v .: "type"+ parseJSON _ = mzero+ -- | A Bugzilla bug. data Bug = Bug { bugId :: !BugId@@ -377,6 +419,7 @@ , bugVersion :: [T.Text] , bugWhiteboard :: T.Text , bugCustomFields :: H.HashMap T.Text T.Text+ , bugExternalBugs :: Maybe [ExternalBug] } deriving (Eq, Show) instance FromJSON Bug where@@ -418,6 +461,7 @@ <*> v .: "version" <*> v .: "whiteboard" <*> pure (customFields v)+ <*> v .:? "external_bugs" parseJSON _ = mzero customFields :: Object -> H.HashMap T.Text T.Text
src/Web/Bugzilla/RedHat/Search.hs view
@@ -22,11 +22,15 @@ , containsAll , changedBefore , changedAfter+, changedSince+, changedUntil+, changedRange , changedFrom , changedTo , changedBy , contentMatches , isEmpty+, isNotEmpty , (.&&.) , (.||.) , not'@@ -94,6 +98,18 @@ changedAfter :: FieldType a => Field a -> UTCTime -> SearchExpression changedAfter = (Term .) . BinaryOp "changedafter" +-- | Filter bug changed since UTCTime+changedSince :: UTCTime -> SearchExpression+changedSince ts = Term $ EqTerm (CustomField "chfieldfrom") ts++-- | Filter bug changed until UTCTime+changedUntil :: UTCTime -> SearchExpression+changedUntil ts = Term $ EqTerm (CustomField "chfieldto") ts++-- | Filter bug changed in range+changedRange :: UTCTime -> UTCTime -> SearchExpression+changedRange from to = changedSince from .&&. changedUntil to+ changedFrom :: FieldType a => Field a -> a -> SearchExpression changedFrom = (Term .) . BinaryOp "changedfrom" @@ -108,6 +124,9 @@ isEmpty :: FieldType a => Field a -> SearchExpression isEmpty = Term . UnaryOp "isempty"++isNotEmpty :: FieldType a => Field a -> SearchExpression+isNotEmpty = Term . UnaryOp "isnotempty" (.&&.) :: SearchExpression -> SearchExpression -> SearchExpression (.&&.) (And as) (And bs) = And (as ++ bs)
+ test/Spec.hs view
@@ -0,0 +1,36 @@+{-# LANGUAGE OverloadedStrings #-}++-- |+module Main (main) where++import Test.Hspec+import Web.Bugzilla.RedHat.Search++main :: IO ()+main = hspec spec++spec :: Spec+spec = describe "unit tests" $ do+ describe "search" $ do+ it "eval isNotEmpty" $ do+ evalSearchExpr (isNotEmpty (CustomField "my_field"))+ `shouldBe` [ ("f1", Just "my_field"),+ ("o1", Just "isnotempty"),+ ("v1", Just "")+ ]+ it "eval changedSince" $ do+ evalSearchExpr (changedSince (read "2021-04-01 00:00:00 UTC"))+ `shouldBe` [("chfieldfrom", Just "2021-04-01T00:00:00Z")]++ it "eval changedRange" $ do+ evalSearchExpr+ ( changedRange+ (read "2021-04-01 00:00:00 UTC")+ (read "2021-04-13 00:00:00 UTC")+ )+ `shouldBe` [ -- TODO: fix unused f1 and f4 query args+ ("f1", Just "OP"),+ ("f4", Just "CP"),+ ("chfieldfrom", Just "2021-04-01T00:00:00Z"),+ ("chfieldto", Just "2021-04-13T00:00:00Z")+ ]