Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
699d6d78dc | ||
|
|
942ba52f56 | ||
|
|
d77b6df851 | ||
|
|
3662c1d428 | ||
|
|
e909ef3e62 | ||
|
|
9a393c8603 | ||
|
|
ea429e077a | ||
|
|
90f9b00e5e | ||
|
|
f06f3e3394 | ||
|
|
88af46fbe3 | ||
|
|
27450fae66 | ||
|
|
da6318f5a2 | ||
|
|
3151aa2ebc | ||
|
|
e1ad5cb1ae | ||
|
|
ead346816b | ||
|
|
b504473790 | ||
|
|
8073c84809 | ||
|
|
d28dee41a1 | ||
|
|
003685ff46 | ||
|
|
61f8f41a36 | ||
|
|
55f6318dcd | ||
|
|
96d108d724 | ||
|
|
d77924a9d1 | ||
|
|
4c74e54ae1 | ||
|
|
591f0eb78d | ||
|
|
c9b960f955 | ||
|
|
23a75aefcc | ||
|
|
3756309b22 | ||
|
|
2ae99daa12 | ||
|
|
796de39762 | ||
|
|
c541f83cef | ||
|
|
a142128915 | ||
|
|
ee8f754d7a | ||
|
|
3d02abc844 | ||
|
|
a049a8d5cd | ||
|
|
2c635cfde8 | ||
|
|
24f70f5bdf | ||
|
|
d7d5473664 | ||
|
|
d7e0fb948e |
@@ -1,21 +1,26 @@
|
|||||||
## Serve a RESTful API from any Postgres database
|

|
||||||
|
|
||||||
[](https://circleci.com/gh/begriffs/postgrest/tree/master)
|
[](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
|
PostgREST serves a fully RESTful API from any existing PostgreSQL
|
||||||
database. It provides a cleaner, more standards-compliant, faster
|
database. It provides a cleaner, more standards-compliant, faster
|
||||||
API than you are likely to write from scratch.
|
API than you are likely to write from scratch.
|
||||||
|
|
||||||
### Demo
|
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](https://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
|
||||||
|
|
||||||
Try making requests to the live [demo server] with an HTTP client
|
Try making requests to the live demo server with an HTTP client
|
||||||
such as [postman](http://www.getpostman.com/).
|
such as [postman](http://www.getpostman.com/). The structure of the
|
||||||
|
demo database is defined by
|
||||||
[video placeholder]
|
[begriffs/postgrest-example](https://github.com/begriffs/postgrest-example).
|
||||||
|
You can use it as inspiration for test-driven server migrations in
|
||||||
|
your own projects.
|
||||||
|
|
||||||
### Usage
|
### Usage
|
||||||
|
|
||||||
Download [binaries for your platform] and invoke the program like so:
|
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:
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
postgrest --db-host localhost --db-port 5432 \
|
postgrest --db-host localhost --db-port 5432 \
|
||||||
@@ -25,29 +30,10 @@ postgrest --db-host localhost --db-port 5432 \
|
|||||||
--port 3000
|
--port 3000
|
||||||
```
|
```
|
||||||
|
|
||||||
### Security
|
|
||||||
|
|
||||||
PostgREST handles authentication (HTTP Basic over SSL) and delegates
|
|
||||||
authorization to the role information defined in the database. This
|
|
||||||
ensures there is a single declarative source of truth for security.
|
|
||||||
When dealing with the database the server assumes the identity of
|
|
||||||
the currently authenticated user, and for the duration of the
|
|
||||||
connection cannot do anything the user themselves couldn't.
|
|
||||||
|
|
||||||
Postgres 9.5 will soon support true [row-level
|
|
||||||
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/).
|
|
||||||
In the meantime what isn't yet implemented can be simulated with
|
|
||||||
triggers and security-barrier views. Because the possible queries
|
|
||||||
to the database are limited to certain templates using
|
|
||||||
[leakproof](http://blog.2ndquadrant.com/how-do-postgresql-security_barrier-views-work/)
|
|
||||||
functions, the trigger workaround does not compromise row-level
|
|
||||||
security.
|
|
||||||
|
|
||||||
For example security patterns see the [security
|
|
||||||
guide](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions).
|
|
||||||
|
|
||||||
### Performance
|
### Performance
|
||||||
|
|
||||||
|
TLDR; subsecond response times for up to 2000 requests/sec on Heroku free tier. ([see the load test](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling))
|
||||||
|
|
||||||
If you're used to servers written in interpreted languages (or named
|
If you're used to servers written in interpreted languages (or named
|
||||||
after precious gems), prepare to be pleasantly surprised by PostgREST
|
after precious gems), prepare to be pleasantly surprised by PostgREST
|
||||||
performance.
|
performance.
|
||||||
@@ -82,6 +68,27 @@ the [performance guide](https://github.com/begriffs/postgrest/wiki/Performance-a
|
|||||||
Other optimizations are possible, and some are outlined in the
|
Other optimizations are possible, and some are outlined in the
|
||||||
[Future Features](#future-features).
|
[Future Features](#future-features).
|
||||||
|
|
||||||
|
### Security
|
||||||
|
|
||||||
|
PostgREST handles authentication (HTTP Basic over SSL) and delegates
|
||||||
|
authorization to the role information defined in the database. This
|
||||||
|
ensures there is a single declarative source of truth for security.
|
||||||
|
When dealing with the database the server assumes the identity of
|
||||||
|
the currently authenticated user, and for the duration of the
|
||||||
|
connection cannot do anything the user themselves couldn't.
|
||||||
|
|
||||||
|
Postgres 9.5 will soon support true [row-level
|
||||||
|
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/).
|
||||||
|
In the meantime what isn't yet implemented can be simulated with
|
||||||
|
triggers and security-barrier views. Because the possible queries
|
||||||
|
to the database are limited to certain templates using
|
||||||
|
[leakproof](http://blog.2ndquadrant.com/how-do-postgresql-security_barrier-views-work/)
|
||||||
|
functions, the trigger workaround does not compromise row-level
|
||||||
|
security.
|
||||||
|
|
||||||
|
For example security patterns see the [security
|
||||||
|
guide](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions).
|
||||||
|
|
||||||
### Versioning
|
### Versioning
|
||||||
|
|
||||||
A robust long-lived API needs the freedom to exist in multiple
|
A robust long-lived API needs the freedom to exist in multiple
|
||||||
@@ -125,17 +132,28 @@ and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
|
|||||||
|
|
||||||
* Watching endpoint changes with sockets and Postgres pubsub
|
* Watching endpoint changes with sockets and Postgres pubsub
|
||||||
* Specifying per-view HTTP caching
|
* Specifying per-view HTTP caching
|
||||||
* Inferring good default caching policies from the Postgres stats
|
* Inferring good default caching policies from the Postgres stats collector
|
||||||
* Generating mock data to test clients
|
* Generating mock data for test clients
|
||||||
* Maintaining separate connection pools per role to avoid "set/reset
|
* Maintaining separate connection pools per role to avoid "set/reset
|
||||||
role" performance penalty
|
role" performance penalty
|
||||||
* Describe more relationships with Link headers
|
* Describe more relationships with Link headers
|
||||||
* Depending on accept headers, render OPTIONS as [RAML] or a
|
* Depending on accept headers, render OPTIONS as [RAML](http://raml.org/) or a
|
||||||
relational diagram
|
relational diagram
|
||||||
* Add two-legged auth with OAuth 1.0a(?)
|
* Add two-legged auth with OAuth 1.0a(?)
|
||||||
* ... the other [issues](https://github.com/begriffs/postgrest/issues)
|
* ... the other [issues](https://github.com/begriffs/postgrest/issues)
|
||||||
|
|
||||||
|
### Guides
|
||||||
|
|
||||||
|
* [Routing](https://github.com/begriffs/postgrest/wiki/Routing)
|
||||||
|
* [Versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning)
|
||||||
|
* [Performance](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling)
|
||||||
|
* [Security](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions)
|
||||||
|
|
||||||
### Thanks
|
### Thanks
|
||||||
|
|
||||||
Thanks to [Adam Baker](https://github.com/adambaker) for code
|
* [Adam Baker](https://github.com/adambaker) for code
|
||||||
contributions and many fundamental design discussions.
|
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
|
||||||
|
|||||||
@@ -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.5.3"
|
||||||
|
},
|
||||||
|
"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,3 +4,7 @@ machine:
|
|||||||
- createdb -O postgrest_test -U ubuntu postgrest_test
|
- createdb -O postgrest_test -U ubuntu postgrest_test
|
||||||
ghc:
|
ghc:
|
||||||
version: 7.8.3
|
version: 7.8.3
|
||||||
|
test:
|
||||||
|
post:
|
||||||
|
- cabal exec hlint src/*.hs test/**/*.hs
|
||||||
|
- cabal exec packdeps postgrest.cabal
|
||||||
|
|||||||
+43
-29
@@ -1,9 +1,13 @@
|
|||||||
name: postgrest
|
name: postgrest
|
||||||
version: 0.2.4.8
|
description: Reads the schema of a PostgreSQL database and creates RESTful routes
|
||||||
synopsis: The database is your api
|
for the tables and views, supporting all HTTP verbs that security
|
||||||
|
permits.
|
||||||
|
version: 0.2.5.3
|
||||||
|
synopsis: REST API for any Postgres database
|
||||||
license: MIT
|
license: MIT
|
||||||
license-file: LICENSE
|
license-file: LICENSE
|
||||||
author: Joe Nelson, Adam Baker
|
author: Joe Nelson, Adam Baker
|
||||||
|
homepage: https://github.com/begriffs/postgrest
|
||||||
maintainer: cred+github@begriffs.com
|
maintainer: cred+github@begriffs.com
|
||||||
category: Web
|
category: Web
|
||||||
build-type: Simple
|
build-type: Simple
|
||||||
@@ -11,13 +15,13 @@ cabal-version: >=1.10
|
|||||||
|
|
||||||
executable postgrest
|
executable postgrest
|
||||||
main-is: Main.hs
|
main-is: Main.hs
|
||||||
ghc-options: -Wall -W -Werror -O2
|
ghc-options: -Wall -W -O2
|
||||||
default-language: Haskell2010
|
default-language: Haskell2010
|
||||||
default-extensions: OverloadedStrings
|
default-extensions: OverloadedStrings, ScopedTypeVariables
|
||||||
other-extensions: QuasiQuotes
|
other-extensions: QuasiQuotes
|
||||||
build-depends: base >=4.6 && <5
|
build-depends: base >=4.6 && <5
|
||||||
, hasql == 0.4.*, hasql-backend
|
, hasql == 0.7.*, hasql-backend
|
||||||
, hasql-postgres == 0.8.*
|
, hasql-postgres == 0.10.*
|
||||||
, warp >= 3.0.2, wai >= 3.0.1
|
, warp >= 3.0.2, wai >= 3.0.1
|
||||||
, wai-extra, wai-cors
|
, wai-extra, wai-cors
|
||||||
, wai-middleware-static >= 0.6.0
|
, wai-middleware-static >= 0.6.0
|
||||||
@@ -26,14 +30,14 @@ executable postgrest
|
|||||||
, scientific, time
|
, scientific, time
|
||||||
, aeson, network >= 2.6
|
, aeson, network >= 2.6
|
||||||
, bytestring, text, split, string-conversions
|
, bytestring, text, split, string-conversions
|
||||||
, stringsearch, parsec
|
, stringsearch
|
||||||
, containers, unordered-containers
|
, containers, unordered-containers
|
||||||
, optparse-applicative >= 0.9.1 && < 0.10
|
, optparse-applicative == 0.11.*
|
||||||
, regex-base, regex-tdfa
|
, regex-base, regex-tdfa
|
||||||
, regex-tdfa-text
|
, regex-tdfa-text
|
||||||
, Ranged-sets
|
, Ranged-sets
|
||||||
, transformers
|
, transformers, MissingH
|
||||||
, bcrypt, base64-string
|
, bcrypt >= 0.0.6, base64-string
|
||||||
, network-uri >= 2.6
|
, network-uri >= 2.6
|
||||||
, resource-pool
|
, resource-pool
|
||||||
, blaze-builder
|
, blaze-builder
|
||||||
@@ -42,46 +46,56 @@ executable postgrest
|
|||||||
Other-Modules: App
|
Other-Modules: App
|
||||||
, Auth
|
, Auth
|
||||||
, Config
|
, Config
|
||||||
, PgStructure
|
, Error
|
||||||
, PgQuery
|
|
||||||
, PgError
|
|
||||||
, RangeQuery
|
|
||||||
, Middleware
|
, Middleware
|
||||||
|
, PgQuery
|
||||||
|
, PgStructure
|
||||||
|
, RangeQuery
|
||||||
|
, Types
|
||||||
hs-source-dirs: src
|
hs-source-dirs: src
|
||||||
|
|
||||||
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
|
default-extensions: OverloadedStrings, ScopedTypeVariables
|
||||||
other-extensions: 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: Spec.hs
|
Main-Is: Main.hs
|
||||||
Other-Modules: App, Auth, Config, Spec, SpecHelper
|
Other-Modules: App
|
||||||
Build-Depends: base, hspec >= 2.0, QuickCheck
|
, 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
|
, hspec-wai >= 0.5.0, hspec-wai-json
|
||||||
, hasql == 0.4.*, hasql-backend
|
, hasql, hasql-backend
|
||||||
, hasql-postgres == 0.8.*
|
, hasql-postgres
|
||||||
, warp >= 3.0.2, wai >= 3.0.1
|
, warp, wai
|
||||||
|
, packdeps, hlint
|
||||||
, HTTP, convertible
|
, HTTP, convertible
|
||||||
, case-insensitive
|
, case-insensitive
|
||||||
, wai-extra, wai-cors, containers
|
, wai-extra, wai-cors, containers
|
||||||
, wai-middleware-static >= 0.6.0
|
, wai-middleware-static
|
||||||
, http-types, scientific, time
|
, http-types, scientific, time
|
||||||
, bytestring, aeson, network >= 2.6
|
, bytestring, aeson, network
|
||||||
, text, optparse-applicative
|
, text, optparse-applicative
|
||||||
, stringsearch, parsec
|
, stringsearch
|
||||||
, unordered-containers
|
, unordered-containers
|
||||||
, regex-base
|
, regex-base
|
||||||
, string-conversions
|
, string-conversions
|
||||||
, http-media, regex-tdfa
|
, http-media, regex-tdfa
|
||||||
, regex-tdfa-text
|
, regex-tdfa-text
|
||||||
, Ranged-sets
|
, Ranged-sets
|
||||||
, transformers
|
, transformers, MissingH, split
|
||||||
, bcrypt
|
, bcrypt, base64-string
|
||||||
, base64-string
|
, network-uri
|
||||||
, split
|
|
||||||
, network-uri >= 2.6
|
|
||||||
, resource-pool
|
, resource-pool
|
||||||
, blaze-builder
|
, blaze-builder
|
||||||
, vector
|
, vector
|
||||||
|
|||||||
+29
-45
@@ -25,17 +25,17 @@ import Network.Wai
|
|||||||
|
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import qualified Data.Vector as V
|
||||||
import qualified Hasql as H
|
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 Auth
|
||||||
import PgQuery
|
import PgQuery
|
||||||
import RangeQuery
|
import RangeQuery
|
||||||
import PgStructure
|
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 =
|
app reqBody req =
|
||||||
case (path, verb) of
|
case (path, verb) of
|
||||||
([], _) -> do
|
([], _) -> do
|
||||||
@@ -54,8 +54,7 @@ app reqBody req =
|
|||||||
then return $ responseLBS status416 [] "HTTP Range error"
|
then return $ responseLBS status416 [] "HTTP Range error"
|
||||||
else do
|
else do
|
||||||
let qt = QualifiedTable schema (cs table)
|
let qt = QualifiedTable schema (cs table)
|
||||||
let select = coerce $
|
let select = B.Stmt "select " V.empty True <>
|
||||||
("select ",[],mempty) <>
|
|
||||||
parentheticT (
|
parentheticT (
|
||||||
whereT qq $ countRows qt
|
whereT qq $ countRows qt
|
||||||
) <> commaq <> (
|
) <> commaq <> (
|
||||||
@@ -65,7 +64,7 @@ app reqBody req =
|
|||||||
. whereT qq
|
. whereT qq
|
||||||
$ selectStar qt
|
$ selectStar qt
|
||||||
)
|
)
|
||||||
row <- H.single select
|
row <- H.maybeEx select
|
||||||
let (tableTotal, queryTotal, body) =
|
let (tableTotal, queryTotal, body) =
|
||||||
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
||||||
from = fromMaybe 0 $ rangeOffset <$> range
|
from = fromMaybe 0 $ rangeOffset <$> range
|
||||||
@@ -102,9 +101,8 @@ app reqBody req =
|
|||||||
([table], "POST") ->
|
([table], "POST") ->
|
||||||
handleJsonObj reqBody $ \obj -> do
|
handleJsonObj reqBody $ \obj -> do
|
||||||
let qt = QualifiedTable schema (cs table)
|
let qt = QualifiedTable schema (cs table)
|
||||||
query = coerce $
|
query = insertInto qt (map cs $ keys obj) (elems obj)
|
||||||
insertInto qt (map cs $ keys obj) (elems obj)
|
row <- H.maybeEx query
|
||||||
row <- H.single query
|
|
||||||
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
|
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
|
||||||
Just inserted = decode (cs insertedJson) :: Maybe Object
|
Just inserted = decode (cs insertedJson) :: Maybe Object
|
||||||
|
|
||||||
@@ -134,7 +132,7 @@ app reqBody req =
|
|||||||
if S.fromList tableCols == S.fromList cols
|
if S.fromList tableCols == S.fromList cols
|
||||||
then do
|
then do
|
||||||
let vals = elems obj
|
let vals = elems obj
|
||||||
H.unit . coerce $ iffNotT
|
H.unitEx $ iffNotT
|
||||||
(whereT qq $ update qt cols vals)
|
(whereT qq $ update qt cols vals)
|
||||||
(insertSelect qt cols vals)
|
(insertSelect qt cols vals)
|
||||||
return $ responseLBS status204 [ jsonH ] ""
|
return $ responseLBS status204 [ jsonH ] ""
|
||||||
@@ -147,12 +145,23 @@ app reqBody req =
|
|||||||
([table], "PATCH") ->
|
([table], "PATCH") ->
|
||||||
handleJsonObj reqBody $ \obj -> do
|
handleJsonObj reqBody $ \obj -> do
|
||||||
let qt = QualifiedTable schema (cs table)
|
let qt = QualifiedTable schema (cs table)
|
||||||
H.unit
|
H.unitEx
|
||||||
$ coerce
|
|
||||||
$ whereT qq
|
$ whereT qq
|
||||||
$ update qt (map cs $ keys obj) (elems obj)
|
$ update qt (map cs $ keys obj) (elems obj)
|
||||||
return $ responseLBS status204 [ jsonH ] ""
|
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 [] ""
|
return $ responseLBS status404 [] ""
|
||||||
|
|
||||||
@@ -164,38 +173,12 @@ app reqBody req =
|
|||||||
schema = requestedSchema hdrs
|
schema = requestedSchema hdrs
|
||||||
range = rangeRequested hdrs
|
range = rangeRequested hdrs
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
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 :: t
|
||||||
isSqlError = Just
|
isSqlError = undefined
|
||||||
|
|
||||||
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)
|
|
||||||
|
|
||||||
|
|
||||||
rangeStatus :: Int -> Int -> Int -> Status
|
rangeStatus :: Int -> Int -> Int -> Status
|
||||||
rangeStatus from to total
|
rangeStatus from to total
|
||||||
@@ -226,8 +209,8 @@ requestedSchema hdrs =
|
|||||||
jsonH :: Header
|
jsonH :: Header
|
||||||
jsonH = (hContentType, "application/json")
|
jsonH = (hContentType, "application/json")
|
||||||
|
|
||||||
handleJsonObj :: BL.ByteString -> (Object -> H.Tx H.Postgres s Response)
|
handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
|
||||||
-> H.Tx H.Postgres s Response
|
-> H.Tx P.Postgres s Response
|
||||||
handleJsonObj reqBody handler = do
|
handleJsonObj reqBody handler = do
|
||||||
let p = eitherDecode reqBody
|
let p = eitherDecode reqBody
|
||||||
case p of
|
case p of
|
||||||
@@ -243,6 +226,7 @@ handleJsonObj reqBody handler = do
|
|||||||
jErr = encode . object $
|
jErr = encode . object $
|
||||||
[("message", String "Expecting a JSON object")]
|
[("message", String "Expecting a JSON object")]
|
||||||
|
|
||||||
|
|
||||||
data TableOptions = TableOptions {
|
data TableOptions = TableOptions {
|
||||||
tblOptcolumns :: [Column]
|
tblOptcolumns :: [Column]
|
||||||
, tblOptpkey :: [Text]
|
, tblOptpkey :: [Text]
|
||||||
|
|||||||
+12
-10
@@ -7,8 +7,10 @@ import Control.Applicative ( (<*>), (<$>) )
|
|||||||
import Crypto.BCrypt
|
import Crypto.BCrypt
|
||||||
import Data.Text
|
import Data.Text
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import qualified Data.Vector as V
|
||||||
import qualified Hasql as H
|
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 Data.String.Conversions (cs)
|
||||||
import PgQuery (pgFmtLit)
|
import PgQuery (pgFmtLit)
|
||||||
|
|
||||||
@@ -45,22 +47,22 @@ data LoginAttempt =
|
|||||||
checkPass :: Text -> Text -> Bool
|
checkPass :: Text -> Text -> Bool
|
||||||
checkPass = (. cs) . validatePassword . cs
|
checkPass = (. cs) . validatePassword . cs
|
||||||
|
|
||||||
setRole :: Text -> H.Tx H.Postgres s ()
|
setRole :: Text -> H.Tx P.Postgres s ()
|
||||||
setRole role = H.unit ("set role " <> cs (pgFmtLit role), [], True)
|
setRole role = H.unitEx $ B.Stmt ("set role " <> cs (pgFmtLit role)) V.empty True
|
||||||
|
|
||||||
resetRole :: H.Tx H.Postgres s ()
|
resetRole :: H.Tx P.Postgres s ()
|
||||||
resetRole = H.unit [H.q|reset role|]
|
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
|
addUser identity pass role = do
|
||||||
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
|
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
|
||||||
H.unit $
|
H.unitEx $
|
||||||
[H.q|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
|
[H.stmt|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
|
||||||
identity (cs hashed :: Text) role
|
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
|
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 ->
|
return $ maybe LoginFailed (\r ->
|
||||||
let (hashed, role) = r in
|
let (hashed, role) = r in
|
||||||
if checkPass hashed pass
|
if checkPass hashed pass
|
||||||
|
|||||||
+9
-9
@@ -24,16 +24,16 @@ data AppConfig = AppConfig {
|
|||||||
|
|
||||||
argParser :: Parser AppConfig
|
argParser :: Parser AppConfig
|
||||||
argParser = AppConfig
|
argParser = AppConfig
|
||||||
<$> strOption (long "db-name" <> short 'd' <> help "name of database")
|
<$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> help "name of database")
|
||||||
<*> option (long "db-port" <> short 'P' <> value 5432 <> help "postgres server port")
|
<*> option auto (long "db-port" <> short 'P' <> metavar "PORT" <> value 5432 <> help "postgres server port" <> showDefault)
|
||||||
<*> strOption (long "db-user" <> short 'U' <> help "postgres authenticator role")
|
<*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> help "postgres authenticator role")
|
||||||
<*> strOption (long "db-pass" <> value "" <> help "password for authenticator role")
|
<*> strOption (long "db-pass" <> metavar "PASS" <> value "" <> help "password for authenticator role")
|
||||||
<*> strOption (long "db-host" <> short 'h' <> value "localhost" <> help "postgres server hostname")
|
<*> 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")
|
<*> 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' <> help "postgres role to use for non-authenticated requests")
|
<*> 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")
|
<*> 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 "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
defaultCorsPolicy :: CorsResourcePolicy
|
||||||
defaultCorsPolicy = CorsResourcePolicy Nothing
|
defaultCorsPolicy = CorsResourcePolicy Nothing
|
||||||
|
|||||||
@@ -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
|
||||||
+23
-17
@@ -4,10 +4,10 @@ import Paths_postgrest (version)
|
|||||||
|
|
||||||
import App
|
import App
|
||||||
import Middleware
|
import Middleware
|
||||||
|
import Error(errResponse)
|
||||||
|
|
||||||
import Control.Monad (unless)
|
import Control.Monad (unless)
|
||||||
import Control.Monad.IO.Class (liftIO)
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Control.Exception
|
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Network.Wai (strictRequestBody)
|
import Network.Wai (strictRequestBody)
|
||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
@@ -17,14 +17,21 @@ import Network.Wai.Middleware.Static (staticPolicy, only)
|
|||||||
import Data.List (intercalate)
|
import Data.List (intercalate)
|
||||||
import Data.Version (versionBranch)
|
import Data.Version (versionBranch)
|
||||||
import qualified Hasql as H
|
import qualified Hasql as H
|
||||||
import qualified Hasql.Postgres as H
|
import qualified Hasql.Postgres as P
|
||||||
import Options.Applicative hiding (columns)
|
import Options.Applicative hiding (columns)
|
||||||
|
|
||||||
import Config (AppConfig(..), argParser, corsPolicy)
|
import Config (AppConfig(..), argParser, corsPolicy)
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
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
|
let port = configPort conf
|
||||||
|
|
||||||
unless (configSecure conf) $
|
unless (configSecure conf) $
|
||||||
@@ -32,16 +39,12 @@ main = do
|
|||||||
Prelude.putStrLn $ "Listening on port " ++
|
Prelude.putStrLn $ "Listening on port " ++
|
||||||
(show $ configPort conf :: String)
|
(show $ configPort conf :: String)
|
||||||
|
|
||||||
let pgSettings = H.ParamSettings (cs $ configDbHost conf)
|
let pgSettings = P.ParamSettings (cs $ configDbHost conf)
|
||||||
(fromIntegral $ configDbPort conf)
|
(fromIntegral $ configDbPort conf)
|
||||||
(cs $ configDbUser conf)
|
(cs $ configDbUser conf)
|
||||||
(cs $ configDbPass conf)
|
(cs $ configDbPass conf)
|
||||||
(cs $ configDbName conf)
|
(cs $ configDbName conf)
|
||||||
|
appSettings = setPort port
|
||||||
sessSettings <- maybe (fail "Improper session settings") return $
|
|
||||||
H.sessionSettings (fromIntegral $ configPool conf) 30
|
|
||||||
|
|
||||||
let appSettings = setPort port
|
|
||||||
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
||||||
$ defaultSettings
|
$ defaultSettings
|
||||||
middle =
|
middle =
|
||||||
@@ -49,15 +52,18 @@ main = do
|
|||||||
. gzip def . cors corsPolicy
|
. gzip def . cors corsPolicy
|
||||||
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
||||||
anonRole = cs $ configAnonRole conf
|
anonRole = cs $ configAnonRole conf
|
||||||
|
currRole = cs $ configDbUser conf
|
||||||
|
|
||||||
H.session pgSettings sessSettings $ H.sessionUnlifter >>= \unlift ->
|
poolSettings <- maybe (fail "Improper session settings") return $
|
||||||
liftIO $ runSettings appSettings $ middle $ \req respond -> do
|
H.poolSettings (fromIntegral $ configPool conf) 30
|
||||||
body <- strictRequestBody req
|
pool :: H.Pool P.Postgres
|
||||||
respond =<< catchJust isSqlError
|
<- H.acquirePool pgSettings poolSettings
|
||||||
(unlift $ H.tx Nothing
|
|
||||||
$ authenticated anonRole (app body) req)
|
runSettings appSettings $ middle $ \req respond -> do
|
||||||
(return . sqlError)
|
body <- strictRequestBody req
|
||||||
|
resOrError <- liftIO $ H.session pool $ H.tx Nothing $
|
||||||
|
authenticated currRole anonRole (app body) req
|
||||||
|
either (respond . errResponse) respond resOrError
|
||||||
|
|
||||||
where
|
where
|
||||||
describe = progDesc "create a REST API to an existing Postgres database"
|
|
||||||
prettyVersion = intercalate "." $ map show $ versionBranch version
|
prettyVersion = intercalate "." $ map show $ versionBranch version
|
||||||
|
|||||||
+9
-21
@@ -9,7 +9,7 @@ import Data.Text
|
|||||||
-- import Data.Pool(withResource, Pool)
|
-- import Data.Pool(withResource, Pool)
|
||||||
|
|
||||||
import qualified Hasql as H
|
import qualified Hasql as H
|
||||||
import qualified Hasql.Postgres as H
|
import qualified Hasql.Postgres as P
|
||||||
import Data.String.Conversions(cs)
|
import Data.String.Conversions(cs)
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (hLocation, hAuthorization)
|
import Network.HTTP.Types.Header (hLocation, hAuthorization)
|
||||||
@@ -22,33 +22,21 @@ import Network.URI (URI(..), parseURI)
|
|||||||
import Auth (LoginAttempt(..), signInRole, setRole, resetRole)
|
import Auth (LoginAttempt(..), signInRole, setRole, resetRole)
|
||||||
import Codec.Binary.Base64.String (decode)
|
import Codec.Binary.Base64.String (decode)
|
||||||
|
|
||||||
-- data Environment = Test | Production deriving (Eq)
|
authenticated :: forall s. Text -> Text ->
|
||||||
|
(Request -> H.Tx P.Postgres s Response) ->
|
||||||
-- safeAction :: Request -> Bool
|
Request -> H.Tx P.Postgres s Response
|
||||||
-- safeAction = (`notElem` ["PATCH", "PUT"]) . requestMethod
|
authenticated currentRole anon app req = do
|
||||||
|
|
||||||
-- withSavepoint :: Environment -> (Connection -> Application) ->
|
|
||||||
-- Connection -> Application
|
|
||||||
-- withSavepoint env app conn req respond =
|
|
||||||
-- if env == Production && safeAction req
|
|
||||||
-- then go
|
|
||||||
-- else Database.PostgreSQL.Simple.withSavepoint conn go
|
|
||||||
-- where go = app conn req respond
|
|
||||||
|
|
||||||
authenticated :: forall s. Text -> (Request -> H.Tx H.Postgres s Response) ->
|
|
||||||
Request -> H.Tx H.Postgres s Response
|
|
||||||
authenticated anon app req = do
|
|
||||||
attempt <- httpRequesterRole (requestHeaders req)
|
attempt <- httpRequesterRole (requestHeaders req)
|
||||||
case attempt of
|
case attempt of
|
||||||
MalformedAuth ->
|
MalformedAuth ->
|
||||||
return $ responseLBS status400 [] "Malformed basic auth header"
|
return $ responseLBS status400 [] "Malformed basic auth header"
|
||||||
LoginFailed ->
|
LoginFailed ->
|
||||||
return $ responseLBS status401 [] "Invalid username or password"
|
return $ responseLBS status401 [] "Invalid username or password"
|
||||||
LoginSuccess role -> runInRole role
|
LoginSuccess role -> if role /= currentRole then runInRole role else app req
|
||||||
NoCredentials -> runInRole anon
|
NoCredentials -> if anon /= currentRole then runInRole anon else app req
|
||||||
|
|
||||||
where
|
where
|
||||||
httpRequesterRole :: RequestHeaders -> H.Tx H.Postgres s LoginAttempt
|
httpRequesterRole :: RequestHeaders -> H.Tx P.Postgres s LoginAttempt
|
||||||
httpRequesterRole hdrs = do
|
httpRequesterRole hdrs = do
|
||||||
let auth = fromMaybe "" $ lookup hAuthorization hdrs
|
let auth = fromMaybe "" $ lookup hAuthorization hdrs
|
||||||
case split (==' ') (cs auth) of
|
case split (==' ') (cs auth) of
|
||||||
@@ -58,7 +46,7 @@ authenticated anon app req = do
|
|||||||
_ -> return MalformedAuth
|
_ -> return MalformedAuth
|
||||||
_ -> return NoCredentials
|
_ -> return NoCredentials
|
||||||
|
|
||||||
runInRole :: Text -> H.Tx H.Postgres s Response
|
runInRole :: Text -> H.Tx P.Postgres s Response
|
||||||
runInRole r = do
|
runInRole r = do
|
||||||
setRole r
|
setRole r
|
||||||
res <- app req
|
res <- app req
|
||||||
|
|||||||
@@ -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
|
|
||||||
+85
-98
@@ -1,17 +1,21 @@
|
|||||||
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
|
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
module PgQuery where
|
module PgQuery where
|
||||||
|
|
||||||
import RangeQuery
|
import RangeQuery
|
||||||
|
|
||||||
import qualified Hasql.Postgres as H
|
import qualified Hasql as H
|
||||||
import qualified Hasql.Backend as H
|
import qualified Hasql.Postgres as P
|
||||||
|
import qualified Hasql.Backend as B
|
||||||
|
|
||||||
import Data.Text hiding (map)
|
import Data.Text hiding (map, empty)
|
||||||
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
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import Data.Monoid
|
import Data.Monoid
|
||||||
|
import Data.Vector (empty)
|
||||||
import Data.Maybe (fromMaybe, mapMaybe)
|
import Data.Maybe (fromMaybe, mapMaybe)
|
||||||
import Data.Functor ( (<$>) )
|
import Data.Functor ( (<$>) )
|
||||||
import Control.Monad (join)
|
import Control.Monad (join)
|
||||||
@@ -20,9 +24,12 @@ import qualified Data.Aeson as JSON
|
|||||||
import qualified Data.List as L
|
import qualified Data.List as L
|
||||||
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
|
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
|
||||||
|
|
||||||
type DynamicSQL = (BS.ByteString, [H.StatementArgument H.Postgres], All)
|
type PStmt = H.Stmt P.Postgres
|
||||||
|
instance Monoid PStmt where
|
||||||
type StatementT = DynamicSQL -> DynamicSQL
|
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 {
|
data QualifiedTable = QualifiedTable {
|
||||||
qtSchema :: Text
|
qtSchema :: Text
|
||||||
@@ -36,16 +43,16 @@ data OrderTerm = OrderTerm {
|
|||||||
|
|
||||||
limitT :: Maybe NonnegRange -> StatementT
|
limitT :: Maybe NonnegRange -> StatementT
|
||||||
limitT r q =
|
limitT r q =
|
||||||
q <> (" LIMIT " <> limit <> " OFFSET " <> offset <> " ", [], mempty)
|
q <> B.Stmt (" LIMIT " <> limit <> " OFFSET " <> offset <> " ") empty True
|
||||||
where
|
where
|
||||||
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
|
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
|
||||||
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
|
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
|
||||||
|
|
||||||
whereT :: Net.Query -> StatementT
|
whereT :: Net.Query -> StatementT
|
||||||
whereT params q =
|
whereT params q =
|
||||||
if L.null params
|
if L.null cols
|
||||||
then q
|
then q
|
||||||
else q <> (" where ",[],mempty) <> conjunction
|
else q <> B.Stmt " where " empty True <> conjunction
|
||||||
where
|
where
|
||||||
cols = [ col | col <- params, fst col `notElem` ["order"] ]
|
cols = [ col | col <- params, fst col `notElem` ["order"] ]
|
||||||
conjunction = mconcat $ L.intersperse andq (map wherePred cols)
|
conjunction = mconcat $ L.intersperse andq (map wherePred cols)
|
||||||
@@ -54,94 +61,85 @@ orderT :: [OrderTerm] -> StatementT
|
|||||||
orderT ts q =
|
orderT ts q =
|
||||||
if L.null ts
|
if L.null ts
|
||||||
then q
|
then q
|
||||||
else q <> (" order by ",[],mempty) <> clause
|
else q <> B.Stmt " order by " empty True <> clause
|
||||||
where
|
where
|
||||||
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
|
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
|
||||||
queryTerm :: OrderTerm -> DynamicSQL
|
queryTerm :: OrderTerm -> PStmt
|
||||||
queryTerm t = (" " <> cs (pgFmtIdent $ otTerm t) <> " "
|
queryTerm t = B.Stmt
|
||||||
<> otDirection t <> " "
|
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
|
||||||
, [], mempty)
|
<> cs (otDirection t) <> " ")
|
||||||
|
empty True
|
||||||
|
|
||||||
parentheticT :: StatementT
|
parentheticT :: StatementT
|
||||||
parentheticT (sql, params, pre) =
|
parentheticT s =
|
||||||
(" (" <> sql <> ") ", params, pre)
|
s { B.stmtTemplate = " (" <> B.stmtTemplate s <> ") " }
|
||||||
|
|
||||||
iffNotT :: DynamicSQL -> StatementT
|
iffNotT :: PStmt -> StatementT
|
||||||
iffNotT (aq, ap, apre) (bq, bp, bpre) =
|
iffNotT (B.Stmt aq ap apre) (B.Stmt bq bp bpre) =
|
||||||
("WITH aaa AS (" <> aq <> " returning *) " <>
|
B.Stmt
|
||||||
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)"
|
("WITH aaa AS (" <> aq <> " returning *) " <>
|
||||||
, ap ++ bp
|
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)")
|
||||||
, All $ getAll apre && getAll bpre
|
(ap <> bp)
|
||||||
)
|
(apre && bpre)
|
||||||
|
|
||||||
countRows :: QualifiedTable -> DynamicSQL
|
countT :: StatementT
|
||||||
countRows t =
|
countT s =
|
||||||
("select count(1) from " <> fromQt t, [], mempty)
|
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 :: StatementT
|
||||||
asJsonWithCount (sql, params, pre) = (
|
asJsonWithCount s = s { B.stmtTemplate =
|
||||||
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from (" <> sql <> ") t"
|
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from ("
|
||||||
, params, pre
|
<> B.stmtTemplate s <> ") t" }
|
||||||
)
|
|
||||||
|
|
||||||
asJsonRow :: StatementT
|
asJsonRow :: StatementT
|
||||||
asJsonRow (sql, params, pre) = (
|
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
|
||||||
"row_to_json(t) from (" <> sql <> ") t", params, pre
|
|
||||||
)
|
|
||||||
|
|
||||||
selectStar :: QualifiedTable -> DynamicSQL
|
selectStar :: QualifiedTable -> PStmt
|
||||||
selectStar t =
|
selectStar t = B.Stmt ("select * from " <> fromQt t) empty True
|
||||||
("select * from " <> fromQt t, [], mempty)
|
|
||||||
|
|
||||||
insertInto :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
|
returningStarT :: StatementT
|
||||||
insertInto t [] _ =
|
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
|
||||||
("insert into " <> fromQt t <> " default values returning *", [], mempty)
|
|
||||||
insertInto t cols vals =
|
deleteFrom :: QualifiedTable -> PStmt
|
||||||
|
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True
|
||||||
|
|
||||||
|
insertInto :: QualifiedTable -> [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 <> " (" <>
|
("insert into " <> fromQt t <> " (" <>
|
||||||
cs (intercalate ", " (map pgFmtIdent cols)) <>
|
intercalate ", " (map pgFmtIdent cols) <>
|
||||||
") values (" <>
|
") values ("
|
||||||
cs (
|
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
|
||||||
intercalate ", " (map
|
<> ") returning row_to_json(" <> fromQt t <> ".*)")
|
||||||
((<> "::unknown") . pgFmtLit . unquoted)
|
empty True
|
||||||
vals)
|
|
||||||
) <> ") returning row_to_json(" <> fromQt t <> ".*)"
|
|
||||||
, []
|
|
||||||
, mempty
|
|
||||||
)
|
|
||||||
|
|
||||||
insertSelect :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
|
insertSelect :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt
|
||||||
insertSelect t [] _ =
|
insertSelect t [] _ = B.Stmt
|
||||||
("insert into " <> fromQt t <> " default values returning *", [], mempty)
|
("insert into " <> fromQt t <> " default values returning *") empty True
|
||||||
insertSelect t cols vals =
|
insertSelect t cols vals = B.Stmt
|
||||||
("insert into " <> fromQt t <> " (" <>
|
("insert into " <> fromQt t <> " ("
|
||||||
cs (intercalate ", " (map pgFmtIdent cols)) <>
|
<> intercalate ", " (map pgFmtIdent cols)
|
||||||
") select " <>
|
<> ") select "
|
||||||
cs (
|
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals))
|
||||||
intercalate ", " (map
|
empty True
|
||||||
((<> "::unknown") . pgFmtLit . unquoted)
|
|
||||||
vals)
|
|
||||||
)
|
|
||||||
, []
|
|
||||||
, mempty
|
|
||||||
)
|
|
||||||
|
|
||||||
update :: QualifiedTable -> [Text] -> [JSON.Value] -> DynamicSQL
|
update :: QualifiedTable -> [Text] -> [JSON.Value] -> PStmt
|
||||||
update t cols vals =
|
update t cols vals = B.Stmt
|
||||||
("update " <> fromQt t <> " set (" <>
|
("update " <> fromQt t <> " set ("
|
||||||
cs (intercalate ", " (map pgFmtIdent cols)) <>
|
<> intercalate ", " (map pgFmtIdent cols)
|
||||||
") = (" <>
|
<> ") = ("
|
||||||
cs (
|
<> intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
|
||||||
intercalate ", " (map
|
<> ")")
|
||||||
((<> "::unknown") . pgFmtLit . unquoted)
|
empty True
|
||||||
vals)
|
|
||||||
) <> ")"
|
|
||||||
, []
|
|
||||||
, mempty
|
|
||||||
)
|
|
||||||
|
|
||||||
wherePred :: Net.QueryItem -> DynamicSQL
|
wherePred :: Net.QueryItem -> PStmt
|
||||||
wherePred (col, predicate) =
|
wherePred (col, predicate) = B.Stmt
|
||||||
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs (pgFmtLit value) <> "::unknown ", [], mempty)
|
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs (pgFmtLit value) <> "::unknown ")
|
||||||
|
empty True
|
||||||
|
|
||||||
where
|
where
|
||||||
opCode:rest = split (=='.') $ cs $ fromMaybe "." predicate
|
opCode:rest = split (=='.') $ cs $ fromMaybe "." predicate
|
||||||
@@ -171,11 +169,11 @@ orderParseTerm s =
|
|||||||
else Nothing
|
else Nothing
|
||||||
_ -> Nothing
|
_ -> Nothing
|
||||||
|
|
||||||
commaq :: DynamicSQL
|
commaq :: PStmt
|
||||||
commaq = (", ", [], mempty)
|
commaq = B.Stmt ", " empty True
|
||||||
|
|
||||||
andq :: DynamicSQL
|
andq :: PStmt
|
||||||
andq = (" and ", [], mempty)
|
andq = B.Stmt " and " empty True
|
||||||
|
|
||||||
pgFmtIdent :: Text -> Text
|
pgFmtIdent :: Text -> Text
|
||||||
pgFmtIdent x =
|
pgFmtIdent x =
|
||||||
@@ -198,8 +196,8 @@ pgFmtLit x =
|
|||||||
trimNullChars :: Text -> Text
|
trimNullChars :: Text -> Text
|
||||||
trimNullChars = Data.Text.takeWhile (/= '\x0')
|
trimNullChars = Data.Text.takeWhile (/= '\x0')
|
||||||
|
|
||||||
fromQt :: QualifiedTable -> BS.ByteString
|
fromQt :: QualifiedTable -> Text
|
||||||
fromQt t = cs $ pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
|
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
|
||||||
|
|
||||||
unquoted :: JSON.Value -> Text
|
unquoted :: JSON.Value -> Text
|
||||||
unquoted (JSON.String t) = t
|
unquoted (JSON.String t) = t
|
||||||
@@ -207,14 +205,3 @@ 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
|
||||||
unquoted (JSON.Bool b) = cs . show $ b
|
unquoted (JSON.Bool b) = cs . show $ b
|
||||||
unquoted _ = ""
|
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
@@ -3,25 +3,21 @@
|
|||||||
module PgStructure where
|
module PgStructure where
|
||||||
|
|
||||||
import PgQuery (QualifiedTable(..))
|
import PgQuery (QualifiedTable(..))
|
||||||
import Data.Functor ( (<$>) )
|
|
||||||
import Data.Text hiding (foldl, map, zipWith, concat)
|
import Data.Text hiding (foldl, map, zipWith, concat)
|
||||||
import Data.Aeson
|
import Data.Aeson
|
||||||
import Data.Functor.Identity
|
import Data.Functor.Identity
|
||||||
import qualified Data.Vector as V
|
|
||||||
import Data.String.Conversions (cs)
|
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 Data.Map as Map
|
||||||
|
|
||||||
import qualified Hasql as H
|
import qualified Hasql as H
|
||||||
import qualified Hasql.Backend as H
|
import qualified Hasql.Postgres as P
|
||||||
import qualified Hasql.Postgres as H
|
|
||||||
|
|
||||||
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
|
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,
|
select kcu.column_name, ccu.table_name AS foreign_table_name,
|
||||||
ccu.column_name AS foreign_column_name
|
ccu.column_name AS foreign_column_name
|
||||||
from information_schema.table_constraints AS tc
|
from information_schema.table_constraints AS tc
|
||||||
@@ -36,58 +32,65 @@ foreignKeys table = do
|
|||||||
|
|
||||||
return $ foldl addKey Map.empty r
|
return $ foldl addKey Map.empty r
|
||||||
where
|
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 :: Text -> H.Tx P.Postgres s [Table]
|
||||||
tables schema =
|
tables schema = do
|
||||||
H.list $ [H.q|
|
rows <- H.listEx $
|
||||||
|
[H.stmt|
|
||||||
select table_schema, table_name,
|
select table_schema, table_name,
|
||||||
is_insertable_into
|
is_insertable_into
|
||||||
from information_schema.tables
|
from information_schema.tables
|
||||||
where table_schema = ?
|
where table_schema = ?
|
||||||
order by table_name
|
order by table_name
|
||||||
|] schema
|
|] schema
|
||||||
|
return $ map tableFromRow rows
|
||||||
|
|
||||||
|
|
||||||
columns :: QualifiedTable -> H.Tx H.Postgres s [Column]
|
columns :: QualifiedTable -> H.Tx P.Postgres s [Column]
|
||||||
columns table = do
|
columns table = do
|
||||||
cols <- H.list $ [H.q|
|
cols <- H.listEx $ [H.stmt|
|
||||||
select info.table_schema as schema, info.table_name as table_name,
|
select info.table_schema as schema, info.table_name as table_name,
|
||||||
info.column_name as name, info.ordinal_position as position,
|
info.column_name as name, info.ordinal_position as position,
|
||||||
info.is_nullable as nullable, info.data_type as col_type,
|
info.is_nullable as nullable, info.data_type as col_type,
|
||||||
info.is_updatable as updatable,
|
info.is_updatable as updatable,
|
||||||
info.character_maximum_length as max_len,
|
info.character_maximum_length as max_len,
|
||||||
info.numeric_precision as precision,
|
info.numeric_precision as precision,
|
||||||
info.column_default as default_value,
|
info.column_default as default_value,
|
||||||
array_to_string(enum_info.vals, ',') as enum
|
array_to_string(enum_info.vals, ',') as enum
|
||||||
from (
|
from (
|
||||||
select table_schema, table_name, column_name, ordinal_position,
|
select table_schema, table_name, column_name, ordinal_position,
|
||||||
is_nullable, data_type, is_updatable,
|
is_nullable, data_type, is_updatable,
|
||||||
character_maximum_length, numeric_precision,
|
character_maximum_length, numeric_precision,
|
||||||
column_default, udt_name
|
column_default, udt_name
|
||||||
from information_schema.columns
|
from information_schema.columns
|
||||||
where table_schema = ? and table_name = ?
|
where table_schema = ? and table_name = ?
|
||||||
) as info
|
) as info
|
||||||
left outer join (
|
left outer join (
|
||||||
select n.nspname as s,
|
select n.nspname as s,
|
||||||
t.typname as n,
|
t.typname as n,
|
||||||
array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals
|
array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals
|
||||||
from pg_type t
|
from pg_type t
|
||||||
join pg_enum e on t.oid = e.enumtypid
|
join pg_enum e on t.oid = e.enumtypid
|
||||||
join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
|
||||||
group by s, n
|
group by s, n
|
||||||
) as enum_info
|
) as enum_info
|
||||||
on (info.udt_name = enum_info.n)
|
on (info.udt_name = enum_info.n)
|
||||||
order by position |] (qtSchema table) (qtName table)
|
order by position |]
|
||||||
|
(qtSchema table) (qtName table)
|
||||||
|
|
||||||
fks <- foreignKeys 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
|
primaryKeyColumns table = do
|
||||||
r :: [Identity Text] <- H.list $ [H.q|
|
r <- H.listEx $ [H.stmt|
|
||||||
select kc.column_name
|
select kc.column_name
|
||||||
from
|
from
|
||||||
information_schema.table_constraints tc,
|
information_schema.table_constraints tc,
|
||||||
@@ -101,9 +104,6 @@ primaryKeyColumns table = do
|
|||||||
return $ map runIdentity r
|
return $ map runIdentity r
|
||||||
|
|
||||||
|
|
||||||
vanishNull :: [a] -> Maybe [a]
|
|
||||||
vanishNull xs = if L.null xs then Nothing else Just xs
|
|
||||||
|
|
||||||
toBool :: Text -> Bool
|
toBool :: Text -> Bool
|
||||||
toBool = (== "YES")
|
toBool = (== "YES")
|
||||||
|
|
||||||
@@ -132,37 +132,21 @@ data Column = Column {
|
|||||||
, colFK :: Maybe ForeignKey
|
, colFK :: Maybe ForeignKey
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
instance H.RowParser H.Postgres Column where
|
tableFromRow :: (Text, Text, Text) -> Table
|
||||||
parseRow r =
|
tableFromRow (s, n, i) = Table s n (toBool i)
|
||||||
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
|
|
||||||
|
|
||||||
|
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
|
instance ToJSON Column where
|
||||||
toJSON c = object [
|
toJSON c = object [
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
+18
-13
@@ -11,16 +11,21 @@ import SpecHelper
|
|||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = before resetDb $ around withApp $
|
spec = beforeAll
|
||||||
describe "authorization" $ do
|
(clearTable "postgrest.auth") . afterAll_ (clearTable "postgrest.auth")
|
||||||
it "hides tables that anonymous does not own" $
|
$ around withApp
|
||||||
get "/authors_only" `shouldRespondWith` 404
|
$ describe "authorization" $ do
|
||||||
it "indicates login failure" $ do
|
|
||||||
let auth = authHeader "postgrest_test_author" "fakefake"
|
it "hides tables that anonymous does not own" $
|
||||||
request methodGet "/authors_only" [auth] ""
|
get "/authors_only" `shouldRespondWith` 404
|
||||||
`shouldRespondWith` 401
|
|
||||||
it "allows users with permissions to see their tables" $ do
|
it "indicates login failure" $ do
|
||||||
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
let auth = authHeader "postgrest_test_author" "fakefake"
|
||||||
let auth = authHeader "jdoe" "1234"
|
request methodGet "/authors_only" [auth] ""
|
||||||
request methodGet "/authors_only" [auth] ""
|
`shouldRespondWith` 401
|
||||||
`shouldRespondWith` 200
|
|
||||||
|
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
|
||||||
|
|||||||
@@ -12,8 +12,7 @@ import Network.HTTP.Types
|
|||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = before resetDb $ around withApp $
|
spec = around withApp $ describe "CORS" $ do
|
||||||
describe "CORS" $ do
|
|
||||||
let preflightHeaders = [
|
let preflightHeaders = [
|
||||||
("Accept", "*/*"),
|
("Accept", "*/*"),
|
||||||
("Origin", "http://example.com"),
|
("Origin", "http://example.com"),
|
||||||
|
|||||||
@@ -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
|
||||||
@@ -16,12 +16,10 @@ import Control.Monad (replicateM_)
|
|||||||
|
|
||||||
import TestTypes(IncPK(..), CompoundPK(..))
|
import TestTypes(IncPK(..), CompoundPK(..))
|
||||||
|
|
||||||
--import Debug.Trace
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = before resetDb $ around withApp $ do
|
spec = afterAll_ resetDb $ around withApp $ do
|
||||||
describe "Posting new record" $ do
|
describe "Posting new record" $ do
|
||||||
it "accepts disparate json types" $ do
|
after_ (clearTable "menagerie") . it "accepts disparate json types" $ do
|
||||||
p <- post "/menagerie"
|
p <- post "/menagerie"
|
||||||
[json| {
|
[json| {
|
||||||
"integer": 13, "double": 3.14159, "varchar": "testing!"
|
"integer": 13, "double": 3.14159, "varchar": "testing!"
|
||||||
@@ -33,7 +31,7 @@ spec = before resetDb $ around withApp $ do
|
|||||||
simpleStatus p `shouldBe` created201
|
simpleStatus p `shouldBe` created201
|
||||||
|
|
||||||
context "with no pk supplied" $ do
|
context "with no pk supplied" $ do
|
||||||
context "into a table with auto-incrementing pk" $
|
context "into a table with auto-incrementing pk" . after_ (clearTable "auto_incrementing_pk") $
|
||||||
it "succeeds with 201 and link" $ do
|
it "succeeds with 201 and link" $ do
|
||||||
p <- post "/auto_incrementing_pk" [json| { "non_nullable_string":"not null"} |]
|
p <- post "/auto_incrementing_pk" [json| { "non_nullable_string":"not null"} |]
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
@@ -52,7 +50,7 @@ spec = before resetDb $ around withApp $ do
|
|||||||
post "/simple_pk" [json| { "extra":"foo"} |]
|
post "/simple_pk" [json| { "extra":"foo"} |]
|
||||||
`shouldRespondWith` 400
|
`shouldRespondWith` 400
|
||||||
|
|
||||||
context "into a table with no pk" $
|
context "into a table with no pk" . after_ (clearTable "no_pk") $
|
||||||
it "succeeds with 201 and a link including all fields" $ do
|
it "succeeds with 201 and a link including all fields" $ do
|
||||||
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
|
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
@@ -60,7 +58,7 @@ spec = before resetDb $ around withApp $ do
|
|||||||
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
|
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
|
||||||
simpleStatus p `shouldBe` created201
|
simpleStatus p `shouldBe` created201
|
||||||
|
|
||||||
context "with compound pk supplied" $
|
context "with compound pk supplied" . after_ (clearTable "compound_pk") $
|
||||||
it "builds response location header appropriately" $
|
it "builds response location header appropriately" $
|
||||||
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
|
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
@@ -101,7 +99,7 @@ spec = before resetDb $ around withApp $ do
|
|||||||
[json| { "k1":12, "k2":42 } |]
|
[json| { "k1":12, "k2":42 } |]
|
||||||
`shouldRespondWith` 400
|
`shouldRespondWith` 400
|
||||||
|
|
||||||
context "specifying every column in the table" $ do
|
context "specifying every column in the table" . after_ (clearTable "compound_pk") $ do
|
||||||
it "can create a new record" $ do
|
it "can create a new record" $ do
|
||||||
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||||
[json| { "k1":12, "k2":42, "extra":3 } |]
|
[json| { "k1":12, "k2":42, "extra":3 } |]
|
||||||
@@ -131,7 +129,7 @@ spec = before resetDb $ around withApp $ do
|
|||||||
let record = head rows
|
let record = head rows
|
||||||
compoundExtra record `shouldBe` Just 5
|
compoundExtra record `shouldBe` Just 5
|
||||||
|
|
||||||
context "with an auto-incrementing primary key" $
|
context "with an auto-incrementing primary key" . after_ (clearTable "auto_incrementing_pk") $
|
||||||
|
|
||||||
it "succeeds with 204" $
|
it "succeeds with 204" $
|
||||||
request methodPut "/auto_incrementing_pk?id=eq.1" []
|
request methodPut "/auto_incrementing_pk?id=eq.1" []
|
||||||
@@ -161,7 +159,8 @@ spec = before resetDb $ around withApp $ do
|
|||||||
[json| { "extra":20 } |]
|
[json| { "extra":20 } |]
|
||||||
`shouldRespondWith` 204
|
`shouldRespondWith` 204
|
||||||
|
|
||||||
context "in a nonempty table" $ do
|
context "in a nonempty table" . before_ (clearTable "items" >> createItems 15) .
|
||||||
|
after_ (clearTable "items") $ do
|
||||||
it "can update a single item" $ do
|
it "can update a single item" $ do
|
||||||
g <- get "/items?id=eq.42"
|
g <- get "/items?id=eq.42"
|
||||||
liftIO $ simpleHeaders g
|
liftIO $ simpleHeaders g
|
||||||
|
|||||||
@@ -6,7 +6,8 @@ import Test.Hspec.Wai
|
|||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = before resetDb $ around withApp $ do
|
spec = beforeAll (clearTable "items" >> createItems 15)
|
||||||
|
. 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
|
||||||
@@ -38,6 +39,9 @@ spec = before resetDb $ around withApp $ do
|
|||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "without other constraints" $
|
||||||
|
get "/items?order=asc.id" `shouldRespondWith` 200
|
||||||
|
|
||||||
describe "Canonical location" $ do
|
describe "Canonical location" $ do
|
||||||
it "Sets Content-Location with alphabetized params" $
|
it "Sets Content-Location with alphabetized params" $
|
||||||
get "/no_pk?b=eq.1&a=eq.1"
|
get "/no_pk?b=eq.1&a=eq.1"
|
||||||
|
|||||||
@@ -8,7 +8,8 @@ import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
|
|||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = before resetDb $ around withApp $
|
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
|
||||||
|
. around withApp $
|
||||||
describe "GET /items" $ do
|
describe "GET /items" $ do
|
||||||
|
|
||||||
context "without range headers" $
|
context "without range headers" $
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import SpecHelper
|
|||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = before resetDb $ around withApp $ do
|
spec = around withApp $ do
|
||||||
describe "GET /" $ do
|
describe "GET /" $ do
|
||||||
it "lists views in schema" $
|
it "lists views in schema" $
|
||||||
request methodGet "/" [] ""
|
request methodGet "/" [] ""
|
||||||
|
|||||||
@@ -0,0 +1,9 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes #-}
|
||||||
|
module Main where
|
||||||
|
|
||||||
|
import Test.Hspec
|
||||||
|
import SpecHelper
|
||||||
|
import Spec
|
||||||
|
|
||||||
|
main :: IO ()
|
||||||
|
main = resetDb >> hspec spec
|
||||||
+1
-1
@@ -1 +1 @@
|
|||||||
{-# OPTIONS_GHC -F -pgmF hspec-discover #-}
|
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --no-main #-}
|
||||||
|
|||||||
+47
-21
@@ -7,12 +7,14 @@ import Test.Hspec
|
|||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
|
||||||
import Hasql as H
|
import Hasql as H
|
||||||
|
import Hasql.Backend as H
|
||||||
import Hasql.Postgres as H
|
import Hasql.Postgres as H
|
||||||
|
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
-- import Control.Exception.Base (bracket, finally)
|
import Data.Monoid
|
||||||
|
import Data.Text hiding (map)
|
||||||
|
import qualified Data.Vector as V
|
||||||
import Control.Monad (void)
|
import Control.Monad (void)
|
||||||
import Control.Exception
|
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
||||||
hRange, hAuthorization)
|
hRange, hAuthorization)
|
||||||
@@ -24,9 +26,10 @@ import qualified Data.ByteString.Char8 as BS
|
|||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
import System.Process (readProcess)
|
import System.Process (readProcess)
|
||||||
|
|
||||||
import App (app, sqlError, isSqlError)
|
import App (app)
|
||||||
import Config (AppConfig(..), corsPolicy)
|
import Config (AppConfig(..), corsPolicy)
|
||||||
import Middleware
|
import Middleware
|
||||||
|
import Error(errResponse)
|
||||||
-- import Auth (addUser)
|
-- import Auth (addUser)
|
||||||
|
|
||||||
isLeft :: Either a b -> Bool
|
isLeft :: Either a b -> Bool
|
||||||
@@ -36,34 +39,41 @@ isLeft _ = False
|
|||||||
cfg :: AppConfig
|
cfg :: AppConfig
|
||||||
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10
|
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10
|
||||||
|
|
||||||
testSettings :: SessionSettings
|
testPoolOpts :: PoolSettings
|
||||||
testSettings = fromMaybe (error "bad settings") $ H.sessionSettings 1 30
|
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
|
||||||
|
|
||||||
pgSettings :: Postgres
|
pgSettings :: H.Settings
|
||||||
pgSettings = H.ParamSettings "localhost" 5432 "postgrest_test" "" "postgrest_test"
|
pgSettings = H.ParamSettings (cs $ configDbHost cfg)
|
||||||
|
(fromIntegral $ configDbPort cfg)
|
||||||
|
(cs $ configDbUser cfg)
|
||||||
|
(cs $ configDbPass cfg)
|
||||||
|
(cs $ configDbName cfg)
|
||||||
|
|
||||||
withApp :: ActionWith Application -> IO ()
|
withApp :: ActionWith Application -> IO ()
|
||||||
withApp perform =
|
withApp perform = do
|
||||||
let anonRole = cs $ configAnonRole cfg in
|
let anonRole = cs $ configAnonRole cfg
|
||||||
perform $ middle $ \req resp ->
|
currRole = cs $ configDbUser cfg
|
||||||
H.session pgSettings testSettings $ H.sessionUnlifter >>= \unlift ->
|
pool :: H.Pool H.Postgres
|
||||||
liftIO $ do
|
<- H.acquirePool pgSettings testPoolOpts
|
||||||
body <- strictRequestBody req
|
|
||||||
resp =<< catchJust isSqlError
|
perform $ middle $ \req resp -> do
|
||||||
(unlift $ H.tx Nothing
|
body <- strictRequestBody req
|
||||||
$ authenticated anonRole (app body) req)
|
result <- liftIO $ H.session pool $ H.tx Nothing
|
||||||
(return . sqlError)
|
$ authenticated currRole anonRole (app body) req
|
||||||
|
either (resp . errResponse) resp result
|
||||||
|
|
||||||
where middle = cors corsPolicy
|
where middle = cors corsPolicy
|
||||||
|
|
||||||
|
|
||||||
resetDb :: IO ()
|
resetDb :: IO ()
|
||||||
resetDb = do
|
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.tx Nothing $ do
|
||||||
H.unit [H.q| drop schema if exists "1" cascade |]
|
H.unitEx [H.stmt| drop schema if exists "1" cascade |]
|
||||||
H.unit [H.q| drop schema if exists private cascade |]
|
H.unitEx [H.stmt| drop schema if exists private cascade |]
|
||||||
H.unit [H.q| drop schema if exists postgrest cascade |]
|
H.unitEx [H.stmt| drop schema if exists postgrest cascade |]
|
||||||
|
|
||||||
loadFixture "roles"
|
loadFixture "roles"
|
||||||
loadFixture "schema"
|
loadFixture "schema"
|
||||||
@@ -88,6 +98,22 @@ authHeader :: String -> String -> Header
|
|||||||
authHeader u p =
|
authHeader u p =
|
||||||
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
|
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
|
||||||
|
|
||||||
|
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 = do
|
||||||
|
pool :: H.Pool H.Postgres
|
||||||
|
<- H.acquirePool pgSettings testPoolOpts
|
||||||
|
void . liftIO $ H.session pool $ H.tx Nothing txn
|
||||||
|
where
|
||||||
|
txn = sequence_ $ map H.unitEx stmts
|
||||||
|
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
|
||||||
|
|
||||||
-- for hspec-wai
|
-- for hspec-wai
|
||||||
pending_ :: WaiSession ()
|
pending_ :: WaiSession ()
|
||||||
pending_ = liftIO Test.Hspec.pending
|
pending_ = liftIO Test.Hspec.pending
|
||||||
|
|||||||
@@ -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
|
|
||||||
]
|
|
||||||
Vendored
-16
@@ -453,22 +453,6 @@ SELECT pg_catalog.setval('has_fk_id_seq', 1, false);
|
|||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO items (id) VALUES (1);
|
INSERT INTO items (id) VALUES (1);
|
||||||
INSERT INTO items (id) VALUES (2);
|
|
||||||
INSERT INTO items (id) VALUES (3);
|
|
||||||
INSERT INTO items (id) VALUES (4);
|
|
||||||
INSERT INTO items (id) VALUES (5);
|
|
||||||
INSERT INTO items (id) VALUES (6);
|
|
||||||
INSERT INTO items (id) VALUES (7);
|
|
||||||
INSERT INTO items (id) VALUES (8);
|
|
||||||
INSERT INTO items (id) VALUES (9);
|
|
||||||
INSERT INTO items (id) VALUES (10);
|
|
||||||
INSERT INTO items (id) VALUES (11);
|
|
||||||
INSERT INTO items (id) VALUES (12);
|
|
||||||
INSERT INTO items (id) VALUES (13);
|
|
||||||
INSERT INTO items (id) VALUES (14);
|
|
||||||
INSERT INTO items (id) VALUES (15);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2339 (class 0 OID 0)
|
-- TOC entry 2339 (class 0 OID 0)
|
||||||
-- Dependencies: 198
|
-- Dependencies: 198
|
||||||
|
|||||||
Reference in New Issue
Block a user