+1
-1
@@ -6,5 +6,5 @@ machine:
|
|||||||
version: 7.8.3
|
version: 7.8.3
|
||||||
test:
|
test:
|
||||||
post:
|
post:
|
||||||
- cabal exec hlint src/*.hs test/**/*.hs
|
- cabal exec hlint -- -X QuasiQuotes src/*.hs test/**/*.hs
|
||||||
- cabal exec packdeps postgrest.cabal
|
- cabal exec packdeps postgrest.cabal
|
||||||
|
|||||||
+2
-4
@@ -17,8 +17,7 @@ executable postgrest
|
|||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
ghc-options: -Wall -W -O2
|
ghc-options: -Wall -W -O2
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings, ScopedTypeVariables
|
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||||
other-extensions: QuasiQuotes
|
|
||||||
build-depends: base >=4.6 && <5
|
build-depends: base >=4.6 && <5
|
||||||
, hasql == 0.7.*, hasql-backend
|
, hasql == 0.7.*, hasql-backend
|
||||||
, hasql-postgres == 0.10.*
|
, hasql-postgres == 0.10.*
|
||||||
@@ -57,8 +56,7 @@ executable postgrest
|
|||||||
Test-Suite spec
|
Test-Suite spec
|
||||||
Type: exitcode-stdio-1.0
|
Type: exitcode-stdio-1.0
|
||||||
Default-Language: Haskell2010
|
Default-Language: Haskell2010
|
||||||
default-extensions: OverloadedStrings, ScopedTypeVariables
|
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||||
other-extensions: QuasiQuotes
|
|
||||||
Hs-Source-Dirs: test, src
|
Hs-Source-Dirs: test, src
|
||||||
ghc-options: -Wall -W -Werror
|
ghc-options: -Wall -W -Werror
|
||||||
Main-Is: Main.hs
|
Main-Is: Main.hs
|
||||||
|
|||||||
+34
-29
@@ -9,7 +9,7 @@ import qualified Hasql as H
|
|||||||
import qualified Hasql.Postgres as P
|
import qualified Hasql.Postgres as P
|
||||||
import qualified Hasql.Backend as B
|
import qualified Hasql.Backend as B
|
||||||
|
|
||||||
import Data.Text hiding (map, empty)
|
import qualified Data.Text as T
|
||||||
import Text.Regex.TDFA ( (=~) )
|
import Text.Regex.TDFA ( (=~) )
|
||||||
import Text.Regex.TDFA.Text ()
|
import Text.Regex.TDFA.Text ()
|
||||||
import qualified Network.HTTP.Types.URI as Net
|
import qualified Network.HTTP.Types.URI as Net
|
||||||
@@ -32,12 +32,12 @@ instance Monoid PStmt where
|
|||||||
type StatementT = PStmt -> PStmt
|
type StatementT = PStmt -> PStmt
|
||||||
|
|
||||||
data QualifiedTable = QualifiedTable {
|
data QualifiedTable = QualifiedTable {
|
||||||
qtSchema :: Text
|
qtSchema :: T.Text
|
||||||
, qtName :: Text
|
, qtName :: T.Text
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
data OrderTerm = OrderTerm {
|
data OrderTerm = OrderTerm {
|
||||||
otTerm :: Text
|
otTerm :: T.Text
|
||||||
, otDirection :: BS.ByteString
|
, otDirection :: BS.ByteString
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -106,33 +106,33 @@ returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
|
|||||||
deleteFrom :: QualifiedTable -> PStmt
|
deleteFrom :: QualifiedTable -> PStmt
|
||||||
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True
|
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True
|
||||||
|
|
||||||
insertInto :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt
|
insertInto :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
|
||||||
insertInto t [] _ = B.Stmt
|
insertInto t [] _ = B.Stmt
|
||||||
("insert into " <> fromQt t <> " default values returning *") empty True
|
("insert into " <> fromQt t <> " default values returning *") empty True
|
||||||
insertInto t cols vals = B.Stmt
|
insertInto t cols vals = B.Stmt
|
||||||
("insert into " <> fromQt t <> " (" <>
|
("insert into " <> fromQt t <> " (" <>
|
||||||
intercalate ", " (map pgFmtIdent cols) <>
|
T.intercalate ", " (map pgFmtIdent cols) <>
|
||||||
") values ("
|
") values ("
|
||||||
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
|
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
|
||||||
<> ") returning row_to_json(" <> fromQt t <> ".*)")
|
<> ") returning row_to_json(" <> fromQt t <> ".*)")
|
||||||
empty True
|
empty True
|
||||||
|
|
||||||
insertSelect :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt
|
insertSelect :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
|
||||||
insertSelect t [] _ = B.Stmt
|
insertSelect t [] _ = B.Stmt
|
||||||
("insert into " <> fromQt t <> " default values returning *") empty True
|
("insert into " <> fromQt t <> " default values returning *") empty True
|
||||||
insertSelect t cols vals = B.Stmt
|
insertSelect t cols vals = B.Stmt
|
||||||
("insert into " <> fromQt t <> " ("
|
("insert into " <> fromQt t <> " ("
|
||||||
<> intercalate ", " (map pgFmtIdent cols)
|
<> T.intercalate ", " (map pgFmtIdent cols)
|
||||||
<> ") select "
|
<> ") select "
|
||||||
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals))
|
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals))
|
||||||
empty True
|
empty True
|
||||||
|
|
||||||
update :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt
|
update :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
|
||||||
update t cols vals = B.Stmt
|
update t cols vals = B.Stmt
|
||||||
("update " <> fromQt t <> " set ("
|
("update " <> fromQt t <> " set ("
|
||||||
<> intercalate ", " (map pgFmtIdent cols)
|
<> T.intercalate ", " (map pgFmtIdent cols)
|
||||||
<> ") = ("
|
<> ") = ("
|
||||||
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
|
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
|
||||||
<> ")")
|
<> ")")
|
||||||
empty True
|
empty True
|
||||||
|
|
||||||
@@ -142,8 +142,11 @@ wherePred (col, predicate) = B.Stmt
|
|||||||
empty True
|
empty True
|
||||||
|
|
||||||
where
|
where
|
||||||
opCode:rest = split (=='.') $ cs $ fromMaybe "." predicate
|
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
|
||||||
value = intercalate "." rest
|
unStarredVal = T.intercalate "." rest
|
||||||
|
star c = if c == '*' then '%' else c
|
||||||
|
value = if opCode == "like" || opCode == "ilike"
|
||||||
|
then T.map star unStarredVal else unStarredVal
|
||||||
op = case opCode of
|
op = case opCode of
|
||||||
"eq" -> "="
|
"eq" -> "="
|
||||||
"gt" -> ">"
|
"gt" -> ">"
|
||||||
@@ -151,17 +154,19 @@ wherePred (col, predicate) = B.Stmt
|
|||||||
"gte" -> ">="
|
"gte" -> ">="
|
||||||
"lte" -> "<="
|
"lte" -> "<="
|
||||||
"neq" -> "<>"
|
"neq" -> "<>"
|
||||||
|
"like"-> "like"
|
||||||
|
"ilike"-> "ilike"
|
||||||
_ -> "="
|
_ -> "="
|
||||||
|
|
||||||
orderParse :: Net.Query -> [OrderTerm]
|
orderParse :: Net.Query -> [OrderTerm]
|
||||||
orderParse q =
|
orderParse q =
|
||||||
mapMaybe orderParseTerm . split (==',') $ cs order
|
mapMaybe orderParseTerm . T.split (==',') $ cs order
|
||||||
where
|
where
|
||||||
order = fromMaybe "" $ join (lookup "order" q)
|
order = fromMaybe "" $ join (lookup "order" q)
|
||||||
|
|
||||||
orderParseTerm :: Text -> Maybe OrderTerm
|
orderParseTerm :: T.Text -> Maybe OrderTerm
|
||||||
orderParseTerm s =
|
orderParseTerm s =
|
||||||
case split (=='.') s of
|
case T.split (=='.') s of
|
||||||
[c,d] ->
|
[c,d] ->
|
||||||
if d `elem` ["asc", "desc"]
|
if d `elem` ["asc", "desc"]
|
||||||
then Just $ OrderTerm c $
|
then Just $ OrderTerm c $
|
||||||
@@ -175,31 +180,31 @@ commaq = B.Stmt ", " empty True
|
|||||||
andq :: PStmt
|
andq :: PStmt
|
||||||
andq = B.Stmt " and " empty True
|
andq = B.Stmt " and " empty True
|
||||||
|
|
||||||
pgFmtIdent :: Text -> Text
|
pgFmtIdent :: T.Text -> T.Text
|
||||||
pgFmtIdent x =
|
pgFmtIdent x =
|
||||||
let escaped = replace "\"" "\"\"" (trimNullChars $ cs x) in
|
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
|
||||||
if escaped =~ danger
|
if escaped =~ danger
|
||||||
then "\"" <> escaped <> "\""
|
then "\"" <> escaped <> "\""
|
||||||
else escaped
|
else escaped
|
||||||
|
|
||||||
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: Text
|
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: T.Text
|
||||||
|
|
||||||
pgFmtLit :: Text -> Text
|
pgFmtLit :: T.Text -> T.Text
|
||||||
pgFmtLit x =
|
pgFmtLit x =
|
||||||
let trimmed = trimNullChars x
|
let trimmed = trimNullChars x
|
||||||
escaped = "'" <> replace "'" "''" trimmed <> "'"
|
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
|
||||||
slashed = replace "\\" "\\\\" escaped in
|
slashed = T.replace "\\" "\\\\" escaped in
|
||||||
cs $ if escaped =~ ("\\\\" :: Text)
|
cs $ if escaped =~ ("\\\\" :: T.Text)
|
||||||
then "E" <> slashed
|
then "E" <> slashed
|
||||||
else slashed
|
else slashed
|
||||||
|
|
||||||
trimNullChars :: Text -> Text
|
trimNullChars :: T.Text -> T.Text
|
||||||
trimNullChars = Data.Text.takeWhile (/= '\x0')
|
trimNullChars = T.takeWhile (/= '\x0')
|
||||||
|
|
||||||
fromQt :: QualifiedTable -> Text
|
fromQt :: QualifiedTable -> T.Text
|
||||||
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
|
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
|
||||||
|
|
||||||
unquoted :: JSON.Value -> Text
|
unquoted :: JSON.Value -> T.Text
|
||||||
unquoted (JSON.String t) = t
|
unquoted (JSON.String t) = t
|
||||||
unquoted (JSON.Number n) =
|
unquoted (JSON.Number n) =
|
||||||
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
|
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
|
||||||
module Feature.AuthSpec where
|
module Feature.AuthSpec where
|
||||||
|
|
||||||
-- {{{ Imports
|
-- {{{ Imports
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
|
||||||
module Feature.InsertSpec where
|
module Feature.InsertSpec where
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|||||||
+40
-11
@@ -2,39 +2,68 @@ module Feature.QuerySpec where
|
|||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Hasql as H
|
||||||
|
import Hasql.Postgres as H
|
||||||
|
import Control.Monad (void)
|
||||||
|
import Data.Text(Text)
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
testSet :: IO ()
|
||||||
|
testSet = do
|
||||||
|
clearTable "items" >> clearTable "no_pk"
|
||||||
|
createItems 15
|
||||||
|
pool <- H.acquirePool pgSettings testPoolOpts
|
||||||
|
void . liftIO $ H.session pool $ H.tx Nothing $ do
|
||||||
|
H.unitEx $ insertNoPk "xyyx" "u"
|
||||||
|
H.unitEx $ insertNoPk "xYYx" "v"
|
||||||
|
|
||||||
|
where
|
||||||
|
insertNoPk :: Text -> Text -> H.Stmt H.Postgres
|
||||||
|
insertNoPk = [H.stmt|insert into "1".no_pk (a, b) values (?,?)|]
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = beforeAll (clearTable "items" >> createItems 15)
|
spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
|
||||||
. afterAll_ (clearTable "items") . around withApp $ do
|
|
||||||
describe "Querying a nonexistent table" $
|
describe "Querying a nonexistent table" $
|
||||||
it "causes a 404" $
|
it "causes a 404" $
|
||||||
get "/faketable" `shouldRespondWith` 404
|
get "/faketable" `shouldRespondWith` 404
|
||||||
|
|
||||||
describe "Filtering response" $
|
describe "Filtering response" $ do
|
||||||
context "column equality" $
|
it "matches with equality" $
|
||||||
|
|
||||||
it "matches the predicate" $
|
|
||||||
get "/items?id=eq.5"
|
get "/items?id=eq.5"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[{\"id\":5}]"
|
matchBody = Just [json| [{"id":5}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "matches with like" $ do
|
||||||
|
get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"}]|]
|
||||||
|
get "/no_pk?a=like.xy*" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"}]|]
|
||||||
|
get "/no_pk?a=like.*YY*" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xYYx","b":"v"}]|]
|
||||||
|
|
||||||
|
it "matches with ilike" $ do
|
||||||
|
get "/no_pk?a=ilike.xy*&order=b.asc" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
|
||||||
|
get "/no_pk?a=ilike.*YY*&order=b.asc" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
|
||||||
|
|
||||||
describe "ordering response" $ do
|
describe "ordering response" $ do
|
||||||
it "by a column asc" $
|
it "by a column asc" $
|
||||||
get "/items?id=lte.2&order=id.asc"
|
get "/items?id=lte.2&order=id.asc"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[{\"id\":1},{\"id\":2}]"
|
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||||
}
|
}
|
||||||
it "by a column desc" $
|
it "by a column desc" $
|
||||||
get "/items?id=lte.2&order=id.desc"
|
get "/items?id=lte.2&order=id.desc"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[{\"id\":2},{\"id\":1}]"
|
matchBody = Just [json| [{"id":2},{"id":1}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||||
}
|
}
|
||||||
@@ -52,9 +81,9 @@ spec = beforeAll (clearTable "items" >> createItems 15)
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "Omits question mark when there are no params" $
|
it "Omits question mark when there are no params" $
|
||||||
get "/no_pk"
|
get "/simple_pk"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[]"
|
matchBody = Just "[]"
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Location" <:> "/no_pk"]
|
, matchHeaders = ["Content-Location" <:> "/simple_pk"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
|
|
||||||
module Feature.StructureSpec where
|
module Feature.StructureSpec where
|
||||||
|
|
||||||
import Test.Hspec hiding (pendingWith)
|
import Test.Hspec hiding (pendingWith)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{-# LANGUAGE QuasiQuotes #-}
|
|
||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
|
|
||||||
|
|
||||||
module SpecHelper where
|
module SpecHelper where
|
||||||
|
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
|
|||||||
Reference in New Issue
Block a user