Compare commits

...
15 Commits
Author SHA1 Message Date
Joe Nelson eafb1a848f Bump minor version, add changelog 2015-02-18 11:39:13 -08:00
Joe Nelson 328f27e7bd Merge pull request #152 from brikou/expose_location_header
Add 'Location' to exposed headers
2015-02-18 08:41:00 -08:00
Brikou Carré a93e6c050d Add 'Location' to exposed headers 2015-02-18 10:22:24 +01:00
Joe Nelson 337b49f386 Expose Content-Range response header (and others) in CORS
Fixes #148
2015-02-16 19:35:25 -08:00
Joe Nelson 92daa7d11a Merge branch 'like'
Fixes #132
2015-02-15 17:37:40 -08:00
Joe Nelson fa48c86195 Logically simplify (i)like test cases 2015-02-15 17:34:26 -08:00
Joe Nelson b88192f95a Remove lint 2015-02-15 16:31:53 -08:00
Joe Nelson 5a1ae934b9 Put array open bracket nearer to the json 2015-02-15 16:14:25 -08:00
Joe Nelson 58f18181c2 Update order by params to new style introduced from master 2015-02-15 16:13:06 -08:00
Joe Nelson eea1bc0cac Quasiquote json to fix vim syntax highlighting 2015-02-15 16:03:31 -08:00
Joe Nelson e37d5d9b25 Style tweak 2015-02-15 15:59:49 -08:00
Adam C. BakerandJoe Nelson 5cc77709ba Add like/ilike 2015-02-15 15:23:18 -08:00
Joe Nelson 5280b9fd6d Merge pull request #138 from jcristovao/patch-1
order does not work
2015-02-12 08:41:40 -08:00
João Cristóvão 9cb1ced010 Change order test to match docs 2015-02-12 09:44:49 +00:00
João Cristóvão 0a3057e92d Update PgQuery.hs
I'm a bit baffled nobody noticed this before :P

Anyhow, without it order does not work.

Thanks,
Cheers
2015-02-11 19:49:31 +00:00
14 changed files with 114 additions and 61 deletions
+15
View File
@@ -0,0 +1,15 @@
# Change Log
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## [0.2.6.0] - 2015-02-18
### Added
- A changelog
- Filter by substring match, e.g. `?col=like.*hello*` (or ilike for
case insensitivity).
- Access-Control-Expose-Headers for CORS
### Fixed
- Make filter position match docs, e.g. `?order=col.asc` rather
than `?order=asc.col`.
+1 -1
View File
@@ -20,7 +20,7 @@ your own projects.
### Usage ### Usage
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.5.3.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.5.3.tar.xz)) and invoke like so: Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.6.0.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.6.0.tar.xz)) and invoke like so:
```bash ```bash
postgrest --db-host localhost --db-port 5432 \ postgrest --db-host localhost --db-port 5432 \
+1 -1
View File
@@ -10,7 +10,7 @@
}, },
"POSTGREST_VER": { "POSTGREST_VER": {
"description": "Version of PostgREST to deploy", "description": "Version of PostgREST to deploy",
"value": "0.2.5.3" "value": "0.2.6.0"
}, },
"DB_NAME": { "DB_NAME": {
"description": "Database name", "description": "Database name",
+1 -1
View File
@@ -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
+3 -5
View File
@@ -2,7 +2,7 @@ name: postgrest
description: Reads the schema of a PostgreSQL database and creates RESTful routes description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security for the tables and views, supporting all HTTP verbs that security
permits. permits.
version: 0.2.5.3 version: 0.2.6.0
synopsis: REST API for any Postgres database synopsis: REST API for any Postgres database
license: MIT license: MIT
license-file: LICENSE license-file: LICENSE
@@ -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
+4
View File
@@ -45,6 +45,10 @@ corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy { Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True) corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders , corsRequestHeaders = "Authentication":accHeaders
, corsExposedHeaders = Just [
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
]
} }
Nothing -> Nothing Nothing -> Nothing
where where
+35 -30
View File
@@ -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,18 +154,20 @@ 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
[d,c] -> [c,d] ->
if d `elem` ["asc", "desc"] if d `elem` ["asc", "desc"]
then Just $ OrderTerm c $ then Just $ OrderTerm c $
if d == "asc" then "asc" else "desc" if d == "asc" then "asc" else "desc"
@@ -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
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.AuthSpec where module Feature.AuthSpec where
-- {{{ Imports -- {{{ Imports
+8
View File
@@ -53,6 +53,14 @@ spec = around withApp $ describe "CORS" $ do
r <- request methodOptions "/" preflightHeaders "" r <- request methodOptions "/" preflightHeaders ""
liftIO $ simpleBody r `shouldBe` "" liftIO $ simpleBody r `shouldBe` ""
describe "regular request" $
it "exposes necesssary response headers" $ do
r <- request methodGet "/items" [("Origin", "http://example.com")] ""
liftIO $ simpleHeaders r `shouldSatisfy` matchHeader
"Access-Control-Expose-Headers"
"Content-Encoding, Content-Location, Content-Range, Content-Type, \
\Date, Location, Server, Transfer-Encoding, Range-Unit"
describe "postflight request" $ describe "postflight request" $
it "allows INFO body through even with CORS request headers present" $ do it "allows INFO body through even with CORS request headers present" $ do
r <- request methodOptions "/items" normalCors "" r <- request methodOptions "/items" normalCors ""
-1
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.InsertSpec where module Feature.InsertSpec where
import Test.Hspec import Test.Hspec
+46 -17
View File
@@ -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" $
get "/items?id=eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
it "matches the predicate" $ it "matches with like" $ do
get "/items?id=eq.5" get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
`shouldRespondWith` ResponseMatcher { [{"a":"xyyx","b":"u"}]|]
matchBody = Just "[{\"id\":5}]" get "/no_pk?a=like.xy*" `shouldRespondWith` [json|
, matchStatus = 200 [{"a":"xyyx","b":"u"}]|]
, matchHeaders = ["Content-Range" <:> "0-0/1"] 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=asc.id" 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=desc.id" 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
View File
@@ -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
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Main where module Main where
import Test.Hspec import Test.Hspec
-2
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
module SpecHelper where module SpecHelper where
import Network.Wai import Network.Wai