Compare commits

...
36 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
Joe Nelson 699d6d78dc Bump patch version 2015-02-07 15:27:56 -08:00
Joe Nelson 942ba52f56 Merge branch 'update-deps' 2015-02-07 14:48:26 -08:00
Joe Nelson d77b6df851 Use new optparse-applicative and bcrypt
Fixes #131 and affects #129
2015-02-07 14:21:40 -08:00
Joe Nelson 3662c1d428 Lint and check for restrictive package locks on ci 2015-02-07 13:48:04 -08:00
Joe Nelson e909ef3e62 Mention all modules in .cabal file
per #129
2015-02-04 14:50:33 -08:00
Joe Nelson 9a393c8603 Bump patch version 2015-01-31 12:48:02 -08:00
Joe Nelson ea429e077a Merge pull request #128 from begriffs/hasql-7
Upgrade to Hasql 0.7
2015-01-31 12:45:45 -08:00
Joe Nelson 90f9b00e5e Include JSON content type for errors 2015-01-31 12:33:44 -08:00
Joe Nelson f06f3e3394 Isolate tests
Credentials from AuthSpec were interfering with StructureSpec
2015-01-31 11:32:08 -08:00
Joe Nelson 88af46fbe3 Reduce test settings duplication
and better variable name
2015-01-31 11:31:11 -08:00
Joe Nelson 27450fae66 Fix NULL input problem
Relates to #127

Still auth problems though
2015-01-28 23:58:17 -08:00
Joe Nelson da6318f5a2 Fix warnings, lint, and ambiguous hasql imports 2015-01-28 20:37:19 -08:00
Adam C. Baker 3151aa2ebc fix table parsing 2015-01-26 18:42:46 -08:00
Adam C. Baker e1ad5cb1ae handle errors in the app too 2015-01-26 18:07:38 -08:00
Adam C. Baker ead346816b reporting errors, but not passing tests. 2015-01-26 17:31:59 -08:00
Joe Nelson b504473790 WIP: Upgrade to hasql 7
Still fails handling query errors
2015-01-25 18:33:50 -08:00
Joe Nelson 8073c84809 More informative cabal file for hackage release 2015-01-17 11:08:46 -08:00
Joe Nelson d28dee41a1 Heroku deployment button on README 2015-01-17 10:46:58 -08:00
Joe Nelson 003685ff46 Add support for DELETE verb
Fixes #125
2015-01-17 00:48:49 -08:00
Joe Nelson 61f8f41a36 Temporarily remove heroku button while I get it working properly 2015-01-07 23:45:58 -08:00
Joe Nelson 55f6318dcd Deploy button and thanks 2015-01-07 23:09:10 -08:00
24 changed files with 588 additions and 503 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`.
+10 -3
View File
@@ -1,6 +1,9 @@
![Logo](static/logo.png "Logo")
[![Build Status](https://circleci.com/gh/begriffs/postgrest.png?circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master)
<a href="https://heroku.com/deploy?template=https://github.com/begriffs/postgrest">
<img src="static/heroku.png" alt="Deploy">
</a>
PostgREST serves a fully RESTful API from any existing PostgreSQL
database. It provides a cleaner, more standards-compliant, faster
@@ -17,7 +20,7 @@ your own projects.
### Usage
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.4.10.tar.xz) / [Ubuntu](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.4.10.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
postgrest --db-host localhost --db-port 5432 \
@@ -148,5 +151,9 @@ and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
### Thanks
Thanks to [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions.
* [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions
* [Nikita Volkov](https://github.com/nikita-volkov) for writing the
wonderful [Hasql](https://github.com/nikita-volkov/hasql) library
and helping me use it
* [Mikey Casalaina](https://github.com/casalaina) for the cool logo
+46
View File
@@ -0,0 +1,46 @@
{
"name": "PostgREST",
"description": "RESTful API for any PostgreSQL database.",
"logo": "https://halcyon.sh/logo.svg",
"repository": "https://github.com/begriffs/postgrest",
"env": {
"BUILDPACK_URL": {
"description": "Heroku buildpack for deploying Haskell applications",
"value": "https://github.com/begriffs/postgrest-heroku"
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.2.6.0"
},
"DB_NAME": {
"description": "Database name",
"required": true
},
"AUTH_ROLE": {
"description": "Database role to use checking client authentication",
"required": true
},
"AUTH_PASS": {
"description": "Authentication password",
"required": false
},
"ANONYMOUS_ROLE": {
"description": "Database role for non-authenticated requests",
"required": true
},
"DB_HOST": {
"description": "Database server hostname",
"required": true
},
"DB_PORT": {
"description": "Database server port",
"required": false,
"value": "5432"
},
"DB_POOL": {
"description": "Maximum number of connections in database pool",
"required": false,
"value": "10"
}
}
}
+4
View File
@@ -4,3 +4,7 @@ machine:
- createdb -O postgrest_test -U ubuntu postgrest_test
ghc:
version: 7.8.3
test:
post:
- cabal exec hlint -- -X QuasiQuotes src/*.hs test/**/*.hs
- cabal exec packdeps postgrest.cabal
+41 -29
View File
@@ -1,9 +1,13 @@
name: postgrest
version: 0.2.4.10
synopsis: The database is your api
description: Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security
permits.
version: 0.2.6.0
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
author: Joe Nelson, Adam Baker
homepage: https://github.com/begriffs/postgrest
maintainer: cred+github@begriffs.com
category: Web
build-type: Simple
@@ -11,13 +15,12 @@ cabal-version: >=1.10
executable postgrest
main-is: Main.hs
ghc-options: -Wall -W -Werror -O2
ghc-options: -Wall -W -O2
default-language: Haskell2010
default-extensions: OverloadedStrings
other-extensions: QuasiQuotes
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
build-depends: base >=4.6 && <5
, hasql == 0.4.*, hasql-backend
, hasql-postgres == 0.8.*
, hasql == 0.7.*, hasql-backend
, hasql-postgres == 0.10.*
, warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors
, wai-middleware-static >= 0.6.0
@@ -26,14 +29,14 @@ executable postgrest
, scientific, time
, aeson, network >= 2.6
, bytestring, text, split, string-conversions
, stringsearch, parsec
, stringsearch
, containers, unordered-containers
, optparse-applicative >= 0.9.1 && < 0.10
, optparse-applicative == 0.11.*
, regex-base, regex-tdfa
, regex-tdfa-text
, Ranged-sets
, transformers
, bcrypt, base64-string
, transformers, MissingH
, bcrypt >= 0.0.6, base64-string
, network-uri >= 2.6
, resource-pool
, blaze-builder
@@ -42,46 +45,55 @@ executable postgrest
Other-Modules: App
, Auth
, Config
, PgStructure
, PgQuery
, PgError
, RangeQuery
, Error
, Middleware
, PgQuery
, PgStructure
, RangeQuery
, Types
hs-source-dirs: src
Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
default-extensions: OverloadedStrings
other-extensions: QuasiQuotes
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
Hs-Source-Dirs: test, src
ghc-options: -Wall -W -Werror
Main-Is: Main.hs
Other-Modules: App, Auth, Config, Spec, SpecHelper
Other-Modules: App
, Auth
, Config
, Error
, Middleware
, PgQuery
, PgStructure
, RangeQuery
, Types
, Spec
, SpecHelper
Build-Depends: base, hspec >= 2.1.2, QuickCheck
, hspec-wai >= 0.5.0, hspec-wai-json
, hasql == 0.4.*, hasql-backend
, hasql-postgres == 0.8.*
, warp >= 3.0.2, wai >= 3.0.1
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, packdeps, hlint
, HTTP, convertible
, case-insensitive
, wai-extra, wai-cors, containers
, wai-middleware-static >= 0.6.0
, wai-middleware-static
, http-types, scientific, time
, bytestring, aeson, network >= 2.6
, bytestring, aeson, network
, text, optparse-applicative
, stringsearch, parsec
, stringsearch
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, regex-tdfa-text
, Ranged-sets
, transformers
, bcrypt
, base64-string
, split
, network-uri >= 2.6
, transformers, MissingH, split
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
+29 -45
View File
@@ -25,17 +25,17 @@ import Network.Wai
import Data.Aeson
import Data.Monoid
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Postgres as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import Auth
import PgQuery
import RangeQuery
import PgStructure
import PgError
import Text.Parsec hiding (Column)
app :: BL.ByteString -> Request -> H.Tx H.Postgres s Response
app :: BL.ByteString -> Request -> H.Tx P.Postgres s Response
app reqBody req =
case (path, verb) of
([], _) -> do
@@ -54,8 +54,7 @@ app reqBody req =
then return $ responseLBS status416 [] "HTTP Range error"
else do
let qt = QualifiedTable schema (cs table)
let select = coerce $
("select ",[],mempty) <>
let select = B.Stmt "select " V.empty True <>
parentheticT (
whereT qq $ countRows qt
) <> commaq <> (
@@ -65,7 +64,7 @@ app reqBody req =
. whereT qq
$ selectStar qt
)
row <- H.single select
row <- H.maybeEx select
let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row
from = fromMaybe 0 $ rangeOffset <$> range
@@ -102,9 +101,8 @@ app reqBody req =
([table], "POST") ->
handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table)
query = coerce $
insertInto qt (map cs $ keys obj) (elems obj)
row <- H.single query
query = insertInto qt (map cs $ keys obj) (elems obj)
row <- H.maybeEx query
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
Just inserted = decode (cs insertedJson) :: Maybe Object
@@ -134,7 +132,7 @@ app reqBody req =
if S.fromList tableCols == S.fromList cols
then do
let vals = elems obj
H.unit . coerce $ iffNotT
H.unitEx $ iffNotT
(whereT qq $ update qt cols vals)
(insertSelect qt cols vals)
return $ responseLBS status204 [ jsonH ] ""
@@ -147,12 +145,23 @@ app reqBody req =
([table], "PATCH") ->
handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table)
H.unit
$ coerce
H.unitEx
$ whereT qq
$ update qt (map cs $ keys obj) (elems obj)
return $ responseLBS status204 [ jsonH ] ""
([table], "DELETE") -> do
let qt = QualifiedTable schema (cs table)
let del = countT
. returningStarT
. whereT qq
$ deleteFrom qt
row <- H.maybeEx del
let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row
return $ if deletedCount == 0
then responseLBS status404 [] ""
else responseLBS status204 [("Content-Range", "*/"<> cs (show deletedCount))] ""
(_, _) ->
return $ responseLBS status404 [] ""
@@ -164,38 +173,12 @@ app reqBody req =
schema = requestedSchema hdrs
range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
coerce (q, args, All b) = (q, args, b)
sqlError :: t
sqlError = undefined
isSqlError :: H.Error -> Maybe H.Error
isSqlError = Just
sqlError :: H.Error -> Response
sqlError err =
let inside = case err of
H.CantConnect _ ->
"Message: \"Cannot connect to postgres server\""
H.ConnectionLost t -> t
H.ErroneousResult t -> t
H.UnexpectedResult t -> t
H.UnparsableTemplate t -> t
H.UnparsableRow t -> t
H.NotInTransaction -> "An operation which requires a"
<> "database transaction was executed without one" in
either
(\hint ->
responseLBS status500
[(hContentType, "application/json")]
(cs . encode . object $ [
("message", String $
"Failed to parse exception:" <> inside)
, ("hint", String . cs . show $ hint)]))
(\msg ->
responseLBS (httpStatus msg)
[(hContentType, "application/json")]
(encode msg))
(parse message "" inside)
isSqlError :: t
isSqlError = undefined
rangeStatus :: Int -> Int -> Int -> Status
rangeStatus from to total
@@ -226,8 +209,8 @@ requestedSchema hdrs =
jsonH :: Header
jsonH = (hContentType, "application/json")
handleJsonObj :: BL.ByteString -> (Object -> H.Tx H.Postgres s Response)
-> H.Tx H.Postgres s Response
handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
-> H.Tx P.Postgres s Response
handleJsonObj reqBody handler = do
let p = eitherDecode reqBody
case p of
@@ -243,6 +226,7 @@ handleJsonObj reqBody handler = do
jErr = encode . object $
[("message", String "Expecting a JSON object")]
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [Text]
+12 -10
View File
@@ -7,8 +7,10 @@ import Control.Applicative ( (<*>), (<$>) )
import Crypto.BCrypt
import Data.Text
import Data.Monoid
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Postgres as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import Data.String.Conversions (cs)
import PgQuery (pgFmtLit)
@@ -45,22 +47,22 @@ data LoginAttempt =
checkPass :: Text -> Text -> Bool
checkPass = (. cs) . validatePassword . cs
setRole :: Text -> H.Tx H.Postgres s ()
setRole role = H.unit ("set role " <> cs (pgFmtLit role), [], True)
setRole :: Text -> H.Tx P.Postgres s ()
setRole role = H.unitEx $ B.Stmt ("set role " <> cs (pgFmtLit role)) V.empty True
resetRole :: H.Tx H.Postgres s ()
resetRole = H.unit [H.q|reset role|]
resetRole :: H.Tx P.Postgres s ()
resetRole = H.unitEx [H.stmt|reset role|]
addUser :: Text -> Text -> Text -> H.Tx H.Postgres s ()
addUser :: Text -> Text -> Text -> H.Tx P.Postgres s ()
addUser identity pass role = do
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
H.unit $
[H.q|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
H.unitEx $
[H.stmt|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
identity (cs hashed :: Text) role
signInRole :: Text -> Text -> H.Tx H.Postgres s LoginAttempt
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
signInRole user pass = do
u <- H.single $ [H.q|select pass, rolname from postgrest.auth where id = ?|] user
u <- H.maybeEx $ [H.stmt|select pass, rolname from postgrest.auth where id = ?|] user
return $ maybe LoginFailed (\r ->
let (hashed, role) = r in
if checkPass hashed pass
+13 -9
View File
@@ -24,16 +24,16 @@ data AppConfig = AppConfig {
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db-name" <> short 'd' <> help "name of database")
<*> option (long "db-port" <> short 'P' <> value 5432 <> help "postgres server port")
<*> strOption (long "db-user" <> short 'U' <> help "postgres authenticator role")
<*> strOption (long "db-pass" <> value "" <> help "password for authenticator role")
<*> strOption (long "db-host" <> short 'h' <> value "localhost" <> help "postgres server hostname")
<$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> help "name of database")
<*> option auto (long "db-port" <> short 'P' <> metavar "PORT" <> value 5432 <> help "postgres server port" <> showDefault)
<*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> help "postgres authenticator role")
<*> strOption (long "db-pass" <> metavar "PASS" <> value "" <> help "password for authenticator role")
<*> strOption (long "db-host" <> metavar "HOST" <> value "localhost" <> help "postgres server hostname" <> showDefault)
<*> option (long "port" <> short 'p' <> value 3000 <> help "port number on which to run HTTP server")
<*> strOption (long "anonymous" <> short 'a' <> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
<*> option (long "db-pool" <> value 10 <> help "Max connections in database pool")
<*> option auto (long "port" <> short 'p' <> metavar "PORT" <> value 3000 <> help "port number on which to run HTTP server" <> showDefault)
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
<*> option auto (long "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
@@ -45,6 +45,10 @@ corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders
, corsExposedHeaders = Just [
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
]
}
Nothing -> Nothing
where
+71
View File
@@ -0,0 +1,71 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
module Error (PgError, errResponse) where
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Network.HTTP.Types.Status as HT
import qualified Data.Aeson as JSON
import qualified Data.Text as T
import Data.Aeson ((.=))
import Data.String.Conversions (cs)
import Data.String.Utils(replace)
import Network.Wai(Response, responseLBS)
import Network.HTTP.Types.Header
type PgError = H.SessionError P.Postgres
errResponse :: PgError -> Response
errResponse e = responseLBS (httpStatus e)
[(hContentType, "application/json")] (JSON.encode e)
instance JSON.ToJSON PgError where
toJSON (H.TxError (P.ErroneousResult c m d h)) = JSON.object [
"code" .= (cs c::T.Text),
"message" .= (cs m::T.Text),
"details" .= (fmap cs d::Maybe T.Text),
"hint" .= (fmap cs h::Maybe T.Text)]
toJSON (H.TxError (P.NoResult d)) = JSON.object [
"message" .= ("No response from server"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.TxError (P.UnexpectedResult m)) = JSON.object ["message" .= m]
toJSON (H.TxError P.NotInTransaction) = JSON.object [
"message" .= ("Not in transaction"::T.Text)]
toJSON (H.CxError (P.CantConnect d)) = JSON.object [
"message" .= ("Can't connect to the database"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.CxError (P.UnsupportedVersion v)) = JSON.object [
"message" .= ("Postgres version "++version++" is not supported") ]
where version = replace "0" "." (show v)
toJSON (H.ResultError m) = JSON.object ["message" .= m]
httpStatus :: PgError -> HT.Status
httpStatus (H.TxError (P.ErroneousResult codeBS _ _ _)) =
let code = cs codeBS in
case code of
'0':'8':_ -> HT.status503 -- pg connection err
'0':'9':_ -> HT.status500 -- triggered action exception
'0':'L':_ -> HT.status403 -- invalid grantor
'0':'P':_ -> HT.status403 -- invalid role specification
'2':'5':_ -> HT.status500 -- invalid tx state
'2':'8':_ -> HT.status403 -- invalid auth specification
'2':'D':_ -> HT.status500 -- invalid tx termination
'3':'8':_ -> HT.status500 -- external routine exception
'3':'9':_ -> HT.status500 -- external routine invocation
'3':'B':_ -> HT.status500 -- savepoint exception
'4':'0':_ -> HT.status500 -- tx rollback
'5':'3':_ -> HT.status503 -- insufficient resources
'5':'4':_ -> HT.status413 -- too complex
'5':'5':_ -> HT.status500 -- obj not on prereq state
'5':'7':_ -> HT.status500 -- operator intervention
'5':'8':_ -> HT.status500 -- system error
'F':'0':_ -> HT.status500 -- conf file error
'H':'V':_ -> HT.status500 -- foreign data wrapper error
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
'X':'X':_ -> HT.status500 -- internal Error
"42P01" -> HT.status404 -- undefined table
"42501" -> HT.status404 -- insufficient privilege
_ -> HT.status400
httpStatus (H.TxError (P.NoResult _)) = HT.status503
httpStatus _ = HT.status500
+22 -17
View File
@@ -4,10 +4,10 @@ import Paths_postgrest (version)
import App
import Middleware
import Error(errResponse)
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Control.Exception
import Data.String.Conversions (cs)
import Network.Wai (strictRequestBody)
import Network.Wai.Middleware.Cors (cors)
@@ -17,14 +17,21 @@ import Network.Wai.Middleware.Static (staticPolicy, only)
import Data.List (intercalate)
import Data.Version (versionBranch)
import qualified Hasql as H
import qualified Hasql.Postgres as H
import qualified Hasql.Postgres as P
import Options.Applicative hiding (columns)
import Config (AppConfig(..), argParser, corsPolicy)
main :: IO ()
main = do
conf <- execParser (info (helper <*> argParser) describe)
let opts = info (helper <*> argParser) $
fullDesc
<> progDesc (
"PostgREST "
<> prettyVersion
<> " / create a REST API to an existing Postgres database"
)
conf <- execParser opts
let port = configPort conf
unless (configSecure conf) $
@@ -32,16 +39,12 @@ main = do
Prelude.putStrLn $ "Listening on port " ++
(show $ configPort conf :: String)
let pgSettings = H.ParamSettings (cs $ configDbHost conf)
let pgSettings = P.ParamSettings (cs $ configDbHost conf)
(fromIntegral $ configDbPort conf)
(cs $ configDbUser conf)
(cs $ configDbPass conf)
(cs $ configDbName conf)
sessSettings <- maybe (fail "Improper session settings") return $
H.sessionSettings (fromIntegral $ configPool conf) 30
let appSettings = setPort port
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle =
@@ -51,14 +54,16 @@ main = do
anonRole = cs $ configAnonRole conf
currRole = cs $ configDbUser conf
H.session pgSettings sessSettings $ H.sessionUnlifter >>= \unlift ->
liftIO $ runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req
respond =<< catchJust isSqlError
(unlift $ H.tx Nothing
$ authenticated currRole anonRole (app body) req)
(return . sqlError)
poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings poolSettings
runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx Nothing $
authenticated currRole anonRole (app body) req
either (respond . errResponse) respond resOrError
where
describe = progDesc "create a REST API to an existing Postgres database"
prettyVersion = intercalate "." $ map show $ versionBranch version
+5 -5
View File
@@ -9,7 +9,7 @@ import Data.Text
-- import Data.Pool(withResource, Pool)
import qualified Hasql as H
import qualified Hasql.Postgres as H
import qualified Hasql.Postgres as P
import Data.String.Conversions(cs)
import Network.HTTP.Types.Header (hLocation, hAuthorization)
@@ -23,8 +23,8 @@ import Auth (LoginAttempt(..), signInRole, setRole, resetRole)
import Codec.Binary.Base64.String (decode)
authenticated :: forall s. Text -> Text ->
(Request -> H.Tx H.Postgres s Response) ->
Request -> H.Tx H.Postgres s Response
(Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response
authenticated currentRole anon app req = do
attempt <- httpRequesterRole (requestHeaders req)
case attempt of
@@ -36,7 +36,7 @@ authenticated currentRole anon app req = do
NoCredentials -> if anon /= currentRole then runInRole anon else app req
where
httpRequesterRole :: RequestHeaders -> H.Tx H.Postgres s LoginAttempt
httpRequesterRole :: RequestHeaders -> H.Tx P.Postgres s LoginAttempt
httpRequesterRole hdrs = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs
case split (==' ') (cs auth) of
@@ -46,7 +46,7 @@ authenticated currentRole anon app req = do
_ -> return MalformedAuth
_ -> return NoCredentials
runInRole :: Text -> H.Tx H.Postgres s Response
runInRole :: Text -> H.Tx P.Postgres s Response
runInRole r = do
setRole r
res <- app req
-86
View File
@@ -1,86 +0,0 @@
{-# LANGUAGE OverloadedStrings #-}
module PgError (Message(..), message, httpStatus) where
import Text.Parsec
import Text.Parsec.Text
import qualified Data.Map as M
import Text.Regex.TDFA.Text ()
import Data.Text hiding (drop, concat, head)
import Data.Aeson
import Data.Maybe
import Control.Monad (void)
import Data.String.Conversions (cs)
import Data.CaseInsensitive (CI, mk)
import Network.HTTP.Types.Status
data Message = Message {
msgStatus :: Maybe Text
, msgCode :: Text
, msgText :: Maybe Text
, msgHint :: Maybe Text
} deriving (Show, Eq)
message :: Parser Message
message = do
ps <- sepBy valPair (char ';')
let m = M.fromList ps
return $ Message
(M.lookup "status" m)
(fromMaybe "" $ M.lookup "code" m)
(M.lookup "message" m)
(M.lookup "hint" m)
valPair :: Parser (CI Text, Text)
valPair = do
_ <- spaces
name <- many1 letter
_ <- char ':'
spaces
_ <- many $ char '"'
val <- manyTill anyChar $
try
(void $ many (char '"') >> (
(void . lookAhead $ (char ';'))
<|> ((optional $ char '.') >> eof)
))
return (mk (cs name), cs val)
instance ToJSON Message where
toJSON t = object [
"message" .= msgText t
, "code" .= msgCode t
, "status" .= msgStatus t
, "hint" .= msgHint t
]
httpStatus :: Message -> Status
httpStatus m =
let code = cs $ msgCode m :: String in
case code of
'0' : '8' : _ -> status503 -- pg connection err
'0' : '9' : _ -> status500 -- triggered action exception
'0' : 'L' : _ -> status403 -- invalid grantor
'0' : 'P' : _ -> status403 -- invalid role specification
'2' : '5' : _ -> status500 -- invalid tx state
'2' : '8' : _ -> status403 -- invalid auth specification
'2' : 'D' : _ -> status500 -- invalid tx termination
'3' : '8' : _ -> status500 -- external routine exception
'3' : '9' : _ -> status500 -- external routine invocation
'3' : 'B' : _ -> status500 -- savepoint exception
'4' : '0' : _ -> status500 -- tx rollback
'5' : '3' : _ -> status503 -- insufficient resources
'5' : '4' : _ -> status413 -- too complex
'5' : '5' : _ -> status500 -- obj not on prereq state
'5' : '7' : _ -> status500 -- operator intervention
'5' : '8' : _ -> status500 -- system error
'F' : '0' : _ -> status500 -- conf file error
'H' : 'V' : _ -> status500 -- foreign data wrapper error
'P' : '0' : _ -> status500 -- PL/pgSQL Error
'X' : 'X' : _ -> status500 -- internal Error
"42P01" -> status404 -- undefined table
"42501" -> status404 -- insufficient privilege
_ -> status400
+108 -116
View File
@@ -1,17 +1,21 @@
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module PgQuery where
import RangeQuery
import qualified Hasql.Postgres as H
import qualified Hasql.Backend as H
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Hasql.Backend as B
import Data.Text hiding (map)
import qualified Data.Text as T
import Text.Regex.TDFA ( (=~) )
import Text.Regex.TDFA.Text ()
import qualified Network.HTTP.Types.URI as Net
import qualified Data.ByteString.Char8 as BS
import Data.Monoid
import Data.Vector (empty)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Functor ( (<$>) )
import Control.Monad (join)
@@ -20,23 +24,26 @@ import qualified Data.Aeson as JSON
import qualified Data.List as L
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
type DynamicSQL = (BS.ByteString, [H.StatementArgument H.Postgres], All)
type StatementT = DynamicSQL -> DynamicSQL
type PStmt = H.Stmt P.Postgres
instance Monoid PStmt where
mappend (B.Stmt query params prep) (B.Stmt query' params' prep') =
B.Stmt (query <> query') (params <> params') (prep && prep')
mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt
data QualifiedTable = QualifiedTable {
qtSchema :: Text
, qtName :: Text
qtSchema :: T.Text
, qtName :: T.Text
} deriving (Show)
data OrderTerm = OrderTerm {
otTerm :: Text
otTerm :: T.Text
, otDirection :: BS.ByteString
}
limitT :: Maybe NonnegRange -> StatementT
limitT r q =
q <> (" LIMIT " <> limit <> " OFFSET " <> offset <> " ", [], mempty)
q <> B.Stmt (" LIMIT " <> limit <> " OFFSET " <> offset <> " ") empty True
where
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
@@ -45,7 +52,7 @@ whereT :: Net.Query -> StatementT
whereT params q =
if L.null cols
then q
else q <> (" where ",[],mempty) <> conjunction
else q <> B.Stmt " where " empty True <> conjunction
where
cols = [ col | col <- params, fst col `notElem` ["order"] ]
conjunction = mconcat $ L.intersperse andq (map wherePred cols)
@@ -54,98 +61,92 @@ orderT :: [OrderTerm] -> StatementT
orderT ts q =
if L.null ts
then q
else q <> (" order by ",[],mempty) <> clause
else q <> B.Stmt " order by " empty True <> clause
where
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
queryTerm :: OrderTerm -> DynamicSQL
queryTerm t = (" " <> cs (pgFmtIdent $ otTerm t) <> " "
<> otDirection t <> " "
, [], mempty)
queryTerm :: OrderTerm -> PStmt
queryTerm t = B.Stmt
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
<> cs (otDirection t) <> " ")
empty True
parentheticT :: StatementT
parentheticT (sql, params, pre) =
(" (" <> sql <> ") ", params, pre)
parentheticT s =
s { B.stmtTemplate = " (" <> B.stmtTemplate s <> ") " }
iffNotT :: DynamicSQL -> StatementT
iffNotT (aq, ap, apre) (bq, bp, bpre) =
("WITH aaa AS (" <> aq <> " returning *) " <>
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)"
, ap ++ bp
, All $ getAll apre && getAll bpre
)
iffNotT :: PStmt -> StatementT
iffNotT (B.Stmt aq ap apre) (B.Stmt bq bp bpre) =
B.Stmt
("WITH aaa AS (" <> aq <> " returning *) " <>
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)")
(ap <> bp)
(apre && bpre)
countRows :: QualifiedTable -> DynamicSQL
countRows t =
("select count(1) from " <> fromQt t, [], mempty)
countT :: StatementT
countT s =
s { B.stmtTemplate = "WITH qqq AS (" <> B.stmtTemplate s <> ") SELECT count(1) FROM qqq" }
countRows :: QualifiedTable -> PStmt
countRows t = B.Stmt ("select count(1) from " <> fromQt t) empty True
asJsonWithCount :: StatementT
asJsonWithCount (sql, params, pre) = (
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from (" <> sql <> ") t"
, params, pre
)
asJsonWithCount s = s { B.stmtTemplate =
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from ("
<> B.stmtTemplate s <> ") t" }
asJsonRow :: StatementT
asJsonRow (sql, params, pre) = (
"row_to_json(t) from (" <> sql <> ") t", params, pre
)
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
selectStar :: QualifiedTable -> DynamicSQL
selectStar t =
("select * from " <> fromQt t, [], mempty)
selectStar :: QualifiedTable -> PStmt
selectStar t = B.Stmt ("select * from " <> fromQt t) empty True
insertInto :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
insertInto t [] _ =
("insert into " <> fromQt t <> " default values returning *", [], mempty)
insertInto t cols vals =
returningStarT :: StatementT
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
deleteFrom :: QualifiedTable -> PStmt
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True
insertInto :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
insertInto t [] _ = B.Stmt
("insert into " <> fromQt t <> " default values returning *") empty True
insertInto t cols vals = B.Stmt
("insert into " <> fromQt t <> " (" <>
cs (intercalate ", " (map pgFmtIdent cols)) <>
") values (" <>
cs (
intercalate ", " (map
((<> "::unknown") . pgFmtLit . unquoted)
vals)
) <> ") returning row_to_json(" <> fromQt t <> ".*)"
, []
, mempty
)
T.intercalate ", " (map pgFmtIdent cols) <>
") values ("
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
<> ") returning row_to_json(" <> fromQt t <> ".*)")
empty True
insertSelect :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
insertSelect t [] _ =
("insert into " <> fromQt t <> " default values returning *", [], mempty)
insertSelect t cols vals =
("insert into " <> fromQt t <> " (" <>
cs (intercalate ", " (map pgFmtIdent cols)) <>
") select " <>
cs (
intercalate ", " (map
((<> "::unknown") . pgFmtLit . unquoted)
vals)
)
, []
, mempty
)
insertSelect :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
insertSelect t [] _ = B.Stmt
("insert into " <> fromQt t <> " default values returning *") empty True
insertSelect t cols vals = B.Stmt
("insert into " <> fromQt t <> " ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") select "
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals))
empty True
update :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
update t cols vals =
("update " <> fromQt t <> " set (" <>
cs (intercalate ", " (map pgFmtIdent cols)) <>
") = (" <>
cs (
intercalate ", " (map
((<> "::unknown") . pgFmtLit . unquoted)
vals)
) <> ")"
, []
, mempty
)
update :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
update t cols vals = B.Stmt
("update " <> fromQt t <> " set ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") = ("
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
<> ")")
empty True
wherePred :: Net.QueryItem -> DynamicSQL
wherePred (col, predicate) =
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs (pgFmtLit value) <> "::unknown ", [], mempty)
wherePred :: Net.QueryItem -> PStmt
wherePred (col, predicate) = B.Stmt
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs (pgFmtLit value) <> "::unknown ")
empty True
where
opCode:rest = split (=='.') $ cs $ fromMaybe "." predicate
value = intercalate "." rest
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
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
"eq" -> "="
"gt" -> ">"
@@ -153,68 +154,59 @@ wherePred (col, predicate) =
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
_ -> "="
orderParse :: Net.Query -> [OrderTerm]
orderParse q =
mapMaybe orderParseTerm . split (==',') $ cs order
mapMaybe orderParseTerm . T.split (==',') $ cs order
where
order = fromMaybe "" $ join (lookup "order" q)
orderParseTerm :: Text -> Maybe OrderTerm
orderParseTerm :: T.Text -> Maybe OrderTerm
orderParseTerm s =
case split (=='.') s of
[d,c] ->
case T.split (=='.') s of
[c,d] ->
if d `elem` ["asc", "desc"]
then Just $ OrderTerm c $
if d == "asc" then "asc" else "desc"
else Nothing
_ -> Nothing
commaq :: DynamicSQL
commaq = (", ", [], mempty)
commaq :: PStmt
commaq = B.Stmt ", " empty True
andq :: DynamicSQL
andq = (" and ", [], mempty)
andq :: PStmt
andq = B.Stmt " and " empty True
pgFmtIdent :: Text -> Text
pgFmtIdent :: T.Text -> T.Text
pgFmtIdent x =
let escaped = replace "\"" "\"\"" (trimNullChars $ cs x) in
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
if escaped =~ danger
then "\"" <> 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 =
let trimmed = trimNullChars x
escaped = "'" <> replace "'" "''" trimmed <> "'"
slashed = replace "\\" "\\\\" escaped in
cs $ if escaped =~ ("\\\\" :: Text)
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
slashed = T.replace "\\" "\\\\" escaped in
cs $ if escaped =~ ("\\\\" :: T.Text)
then "E" <> slashed
else slashed
trimNullChars :: Text -> Text
trimNullChars = Data.Text.takeWhile (/= '\x0')
trimNullChars :: T.Text -> T.Text
trimNullChars = T.takeWhile (/= '\x0')
fromQt :: QualifiedTable -> BS.ByteString
fromQt t = cs $ pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
fromQt :: QualifiedTable -> T.Text
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
unquoted :: JSON.Value -> Text
unquoted :: JSON.Value -> T.Text
unquoted (JSON.String t) = t
unquoted (JSON.Number n) =
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
unquoted (JSON.Bool b) = cs . show $ b
unquoted _ = ""
pgParam :: JSON.Value -> H.StatementArgument H.Postgres
pgParam (JSON.Number n) = H.renderValue
(cs $ formatScientific Fixed
(if isInteger n then Just 0 else Nothing) n :: Text)
pgParam (JSON.String s) = H.renderValue s
pgParam (JSON.Bool b) = H.renderValue $
if b then "t" else "f" :: Text
pgParam JSON.Null = H.renderValue (Nothing :: Maybe Text)
pgParam (JSON.Object o) = H.renderValue $ JSON.encode o
pgParam (JSON.Array a) = H.renderValue $ JSON.encode a
+61 -77
View File
@@ -3,25 +3,21 @@
module PgStructure where
import PgQuery (QualifiedTable(..))
import Data.Functor ( (<$>) )
import Data.Text hiding (foldl, map, zipWith, concat)
import Data.Aeson
import Data.Functor.Identity
import qualified Data.Vector as V
import Data.String.Conversions (cs)
import Data.Maybe (fromMaybe)
import Control.Applicative ( (<$>) )
import Control.Applicative ( (<*>) )
import qualified Data.List as L
import qualified Data.Map as Map
import qualified Hasql as H
import qualified Hasql.Backend as H
import qualified Hasql.Postgres as H
import qualified Hasql.Postgres as P
foreignKeys :: QualifiedTable -> H.Tx H.Postgres s (Map.Map Text ForeignKey)
foreignKeys :: QualifiedTable -> H.Tx P.Postgres s (Map.Map Text ForeignKey)
foreignKeys table = do
r :: [(Text, Text, Text)] <- H.list $ [H.q|
r <- H.listEx $ [H.stmt|
select kcu.column_name, ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
from information_schema.table_constraints AS tc
@@ -36,58 +32,65 @@ foreignKeys table = do
return $ foldl addKey Map.empty r
where
addKey m (col, ftab, fcol) = Map.insert col (ForeignKey (cs ftab) (cs fcol)) m
addKey :: Map.Map Text ForeignKey -> (Text, Text, Text) -> Map.Map Text ForeignKey
addKey m (col, ftab, fcol) = Map.insert col (ForeignKey ftab fcol) m
tables :: Text -> H.Tx H.Postgres s [Table]
tables schema =
H.list $ [H.q|
tables :: Text -> H.Tx P.Postgres s [Table]
tables schema = do
rows <- H.listEx $
[H.stmt|
select table_schema, table_name,
is_insertable_into
from information_schema.tables
where table_schema = ?
order by table_name
|] schema
return $ map tableFromRow rows
columns :: QualifiedTable -> H.Tx H.Postgres s [Column]
columns :: QualifiedTable -> H.Tx P.Postgres s [Column]
columns table = do
cols <- H.list $ [H.q|
select info.table_schema as schema, info.table_name as table_name,
info.column_name as name, info.ordinal_position as position,
info.is_nullable as nullable, info.data_type as col_type,
info.is_updatable as updatable,
info.character_maximum_length as max_len,
info.numeric_precision as precision,
info.column_default as default_value,
array_to_string(enum_info.vals, ',') as enum
from (
select table_schema, table_name, column_name, ordinal_position,
is_nullable, data_type, is_updatable,
character_maximum_length, numeric_precision,
column_default, udt_name
from information_schema.columns
where table_schema = ? and table_name = ?
) as info
left outer join (
select n.nspname as s,
t.typname as n,
array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals
from pg_type t
join pg_enum e on t.oid = e.enumtypid
join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
group by s, n
) as enum_info
on (info.udt_name = enum_info.n)
order by position |] (qtSchema table) (qtName table)
cols <- H.listEx $ [H.stmt|
select info.table_schema as schema, info.table_name as table_name,
info.column_name as name, info.ordinal_position as position,
info.is_nullable as nullable, info.data_type as col_type,
info.is_updatable as updatable,
info.character_maximum_length as max_len,
info.numeric_precision as precision,
info.column_default as default_value,
array_to_string(enum_info.vals, ',') as enum
from (
select table_schema, table_name, column_name, ordinal_position,
is_nullable, data_type, is_updatable,
character_maximum_length, numeric_precision,
column_default, udt_name
from information_schema.columns
where table_schema = ? and table_name = ?
) as info
left outer join (
select n.nspname as s,
t.typname as n,
array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals
from pg_type t
join pg_enum e on t.oid = e.enumtypid
join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
group by s, n
) as enum_info
on (info.udt_name = enum_info.n)
order by position |]
(qtSchema table) (qtName table)
fks <- foreignKeys table
return $ map (\col -> col { colFK = Map.lookup (cs . colName $ col) fks }) cols
return $ map (addFK fks . columnFromRow) cols
where
addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks }
primaryKeyColumns :: QualifiedTable -> H.Tx H.Postgres s [Text]
primaryKeyColumns :: QualifiedTable -> H.Tx P.Postgres s [Text]
primaryKeyColumns table = do
r :: [Identity Text] <- H.list $ [H.q|
r <- H.listEx $ [H.stmt|
select kc.column_name
from
information_schema.table_constraints tc,
@@ -101,9 +104,6 @@ primaryKeyColumns table = do
return $ map runIdentity r
vanishNull :: [a] -> Maybe [a]
vanishNull xs = if L.null xs then Nothing else Just xs
toBool :: Text -> Bool
toBool = (== "YES")
@@ -132,37 +132,21 @@ data Column = Column {
, colFK :: Maybe ForeignKey
} deriving (Show)
instance H.RowParser H.Postgres Column where
parseRow r =
let schema = H.parseResult $ r V.! 0
table = H.parseResult $ r V.! 1
name = H.parseResult $ r V.! 2
position = H.parseResult $ r V.! 3
nullable = toBool <$> (H.parseResult $ r V.! 4 :: Either Text Text)
typ = H.parseResult $ r V.! 5
updatable = toBool <$> (H.parseResult $ r V.! 6 :: Either Text Text)
maxLen = H.parseResult $ r V.! 7
precision = H.parseResult $ r V.! 8
defValue = H.parseResult $ r V.! 9
enum = either (const $ Right []) (Right . split (==','))
(H.parseResult $ r V.! 10 :: Either Text Text)
in
if V.length r /= 11
then Left "Wrong number of fields in Column"
else Column <$> schema <*> table <*> name <*> position <*> nullable
<*> typ <*> updatable <*> maxLen <*> precision
<*> defValue <*> enum
<*> return Nothing
tableFromRow :: (Text, Text, Text) -> Table
tableFromRow (s, n, i) = Table s n (toBool i)
columnFromRow :: (Text, Text, Text,
Int, Text, Text,
Text, Maybe Int, Maybe Int,
Maybe Text, Maybe Text)
-> Column
columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) =
Column s t n pos (toBool nul) typ (toBool u) l p d (parseEnum e) Nothing
where
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
instance H.RowParser H.Postgres Table where
parseRow r =
let schema = H.parseResult $ r V.! 0
name = H.parseResult $ r V.! 1
insertable = toBool <$> (H.parseResult $ r V.! 2 :: Either Text Text) in
if V.length r /= 3
then Left "Wrong number of fields in Table"
else Table <$> schema <*> name <*> insertable
instance ToJSON Column where
toJSON c = object [
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 2.9 KiB

+18 -13
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.AuthSpec where
-- {{{ Imports
@@ -11,15 +10,21 @@ import SpecHelper
-- }}}
spec :: Spec
spec = around withApp $ describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
it "indicates login failure" $ do
let auth = authHeader "postgrest_test_author" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "allows users with permissions to see their tables" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeader "jdoe" "1234"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
spec = beforeAll
(clearTable "postgrest.auth") . afterAll_ (clearTable "postgrest.auth")
$ around withApp
$ describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
it "indicates login failure" $ do
let auth = authHeader "postgrest_test_author" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "allows users with permissions to see their tables" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeader "jdoe" "1234"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+8
View File
@@ -53,6 +53,14 @@ spec = around withApp $ describe "CORS" $ do
r <- request methodOptions "/" preflightHeaders ""
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" $
it "allows INFO body through even with CORS request headers present" $ do
r <- request methodOptions "/items" normalCors ""
+37
View File
@@ -0,0 +1,37 @@
module Feature.DeleteSpec where
import Test.Hspec
import Test.Hspec.Wai
import SpecHelper
import Network.HTTP.Types
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
describe "Deleting" $ do
context "existing record" $ do
it "succeeds with 204 and deletion count" $
request methodDelete "/items?id=eq.1" [] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 204
, matchHeaders = ["Content-Range" <:> "*/1"]
}
it "actually clears items ouf the db" $ do
_ <- request methodDelete "/items?id=lt.15" [] ""
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":15}]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
context "known route, unknown record" $
it "fails with 404" $
request methodDelete "/items?id=eq.101" [] "" `shouldRespondWith` 404
context "totally unknown route" $
it "fails with 404" $
request methodDelete "/foozle?id=eq.101" [] "" `shouldRespondWith` 404
+1 -4
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Feature.InsertSpec where
import Test.Hspec
@@ -16,10 +15,8 @@ import Control.Monad (replicateM_)
import TestTypes(IncPK(..), CompoundPK(..))
--import Debug.Trace
spec :: Spec
spec = around withApp $ do
spec = afterAll_ resetDb $ around withApp $ do
describe "Posting new record" $ do
after_ (clearTable "menagerie") . it "accepts disparate json types" $ do
p <- post "/menagerie"
+46 -17
View File
@@ -2,39 +2,68 @@ module Feature.QuerySpec where
import Test.Hspec
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
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 = beforeAll (clearTable "items" >> createItems 15)
. afterAll_ (clearTable "items") . around withApp $ do
spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
describe "Querying a nonexistent table" $
it "causes a 404" $
get "/faketable" `shouldRespondWith` 404
describe "Filtering response" $
context "column equality" $
describe "Filtering response" $ do
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" $
get "/items?id=eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":5}]"
, matchStatus = 200
, 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
it "by a column asc" $
get "/items?id=lte.2&order=asc.id"
get "/items?id=lte.2&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":1},{\"id\":2}]"
matchBody = Just [json| [{"id":1},{"id":2}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "by a column desc" $
get "/items?id=lte.2&order=desc.id"
get "/items?id=lte.2&order=id.desc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":2},{\"id\":1}]"
matchBody = Just [json| [{"id":2},{"id":1}] |]
, matchStatus = 200
, 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" $
get "/no_pk"
get "/simple_pk"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, 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
import Test.Hspec hiding (pendingWith)
-1
View File
@@ -1,4 +1,3 @@
{-# LANGUAGE QuasiQuotes #-}
module Main where
import Test.Hspec
+41 -29
View File
@@ -1,5 +1,3 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings #-}
module SpecHelper where
import Network.Wai
@@ -7,13 +5,14 @@ import Test.Hspec
import Test.Hspec.Wai
import Hasql as H
import Hasql.Backend as H
import Hasql.Postgres as H
import Data.String.Conversions (cs)
import Data.Monoid
-- import Control.Exception.Base (bracket, finally)
import Data.Text hiding (map)
import qualified Data.Vector as V
import Control.Monad (void)
import Control.Exception
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
hRange, hAuthorization)
@@ -25,9 +24,10 @@ import qualified Data.ByteString.Char8 as BS
import Network.Wai.Middleware.Cors (cors)
import System.Process (readProcess)
import App (app, sqlError, isSqlError)
import App (app)
import Config (AppConfig(..), corsPolicy)
import Middleware
import Error(errResponse)
-- import Auth (addUser)
isLeft :: Either a b -> Bool
@@ -37,35 +37,41 @@ isLeft _ = False
cfg :: AppConfig
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10
testSettings :: SessionSettings
testSettings = fromMaybe (error "bad settings") $ H.sessionSettings 1 30
testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
pgSettings :: Postgres
pgSettings = H.ParamSettings "localhost" 5432 "postgrest_test" "" "postgrest_test"
pgSettings :: H.Settings
pgSettings = H.ParamSettings (cs $ configDbHost cfg)
(fromIntegral $ configDbPort cfg)
(cs $ configDbUser cfg)
(cs $ configDbPass cfg)
(cs $ configDbName cfg)
withApp :: ActionWith Application -> IO ()
withApp perform =
withApp perform = do
let anonRole = cs $ configAnonRole cfg
currRole = cs $ configDbUser cfg in
perform $ middle $ \req resp ->
H.session pgSettings testSettings $ H.sessionUnlifter >>= \unlift ->
liftIO $ do
body <- strictRequestBody req
resp =<< catchJust isSqlError
(unlift $ H.tx Nothing
$ authenticated currRole anonRole (app body) req)
(return . sqlError)
currRole = cs $ configDbUser cfg
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
perform $ middle $ \req resp -> do
body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx Nothing
$ authenticated currRole anonRole (app body) req
either (resp . errResponse) resp result
where middle = cors corsPolicy
resetDb :: IO ()
resetDb = do
H.session pgSettings testSettings $
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $
H.tx Nothing $ do
H.unit [H.q| drop schema if exists "1" cascade |]
H.unit [H.q| drop schema if exists private cascade |]
H.unit [H.q| drop schema if exists postgrest cascade |]
H.unitEx [H.stmt| drop schema if exists "1" cascade |]
H.unitEx [H.stmt| drop schema if exists private cascade |]
H.unitEx [H.stmt| drop schema if exists postgrest cascade |]
loadFixture "roles"
loadFixture "schema"
@@ -90,15 +96,21 @@ authHeader :: String -> String -> Header
authHeader u p =
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
clearTable :: BS.ByteString -> IO ()
clearTable table = H.session pgSettings testSettings $ H.tx Nothing $
H.unit ("delete from \"1\"."<>table, [], True)
clearTable :: Text -> IO ()
clearTable table = do
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ H.Stmt ("delete from \"1\"."<>table) V.empty True
createItems :: Int -> IO ()
createItems n = H.session pgSettings testSettings $ H.tx Nothing txn
createItems n = do
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = sequence_ $ map H.unit stmts
stmts = map [H.q|insert into "1".items (id) values (?)|] [1..n]
txn = sequence_ $ map H.unitEx stmts
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
-- for hspec-wai
pending_ :: WaiSession ()
-41
View File
@@ -1,41 +0,0 @@
module Unit.ErrorsSpec where
import Test.Hspec
import Text.Parsec
import PgError
import Data.Either (rights)
spec :: Spec
spec =
describe "Parsing Hasql errors" $ do
it "can handle status and code" $
let p = parse message "" "Status: \"foo\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "foo") "abc" Nothing Nothing
]
it "can handle weird redundant quotes in status" $
let p = parse message "" "Status: \"\"foo\"\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "foo") "abc" Nothing Nothing
]
it "can handle text and code" $
let p = parse message "" "Message: \"foo\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message Nothing "abc" (Just "foo") Nothing
]
it "can handle status, text and code" $
let p = parse message "" "Status: \"hi\"; Message: \"foo\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "hi") "abc" (Just "foo") Nothing
]
it "can handle unescaped quotes in message" $
let p = parse message "" "Status: \"hi\"; Message: \"unknown \"foo\"!\"; Code: \"abc\"." in
rights [p] `shouldBe` [
Message (Just "hi") "abc" (Just "unknown \"foo\"!") Nothing
]
it "can handle periods in message" $
let p = parse message "" "Message: \"unknown \"foo\".bar\"; Code: \"42P01\"." in
rights [p] `shouldBe` [
Message Nothing "42P01" (Just "unknown \"foo\".bar") Nothing
]