Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
6e6769ea16 | ||
|
|
1e417aef90 | ||
|
|
60eb826cb0 | ||
|
|
3d3c7277c7 | ||
|
|
5be0b5d5ae | ||
|
|
11e5918690 | ||
|
|
c207e7ee30 | ||
|
|
6474a83221 | ||
|
|
9b15071f23 | ||
|
|
51b379d78b | ||
|
|
cac5daca01 | ||
|
|
4bc66c4ca3 | ||
|
|
eafb1a848f | ||
|
|
328f27e7bd | ||
|
|
a93e6c050d | ||
|
|
337b49f386 | ||
|
|
92daa7d11a | ||
|
|
fa48c86195 | ||
|
|
b88192f95a | ||
|
|
5a1ae934b9 | ||
|
|
58f18181c2 | ||
|
|
eea1bc0cac | ||
|
|
e37d5d9b25 | ||
|
|
5cc77709ba | ||
|
|
5280b9fd6d | ||
|
|
9cb1ced010 | ||
|
|
0a3057e92d | ||
|
|
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 | ||
|
|
8200f54ae4 | ||
|
|
5eca8668ef | ||
|
|
30e9733719 | ||
|
|
0830772384 | ||
|
|
2bf45d4f02 | ||
|
|
f689ce684a | ||
|
|
07b039370d | ||
|
|
dbe1f59066 | ||
|
|
5f4212e2fd | ||
|
|
9712549d64 | ||
|
|
19b62c00ee | ||
|
|
d763e27bb2 | ||
|
|
4f9132af43 | ||
|
|
638cd3d49d | ||
|
|
b302843e10 | ||
|
|
1fd757c2ad | ||
|
|
782d6520a2 | ||
|
|
4d8c508a17 | ||
|
|
beb61d8a05 | ||
|
|
c0e4d64423 | ||
|
|
605372f829 | ||
|
|
7d58097449 | ||
|
|
a45718e992 | ||
|
|
13bc9a4510 | ||
|
|
fb6e297e88 | ||
|
|
5900475460 | ||
|
|
4eb4d8da1e | ||
|
|
f1ecbec543 | ||
|
|
42faaa97c9 | ||
|
|
e3d3a07d14 | ||
|
|
ca4138df2a | ||
|
|
0ebb83de3a | ||
|
|
dc43bcbca9 | ||
|
|
8e9bbe6dcb | ||
|
|
eb7e9586ab | ||
|
|
21b0b02c03 | ||
|
|
dba17b895e | ||
|
|
d6e3526bff | ||
|
|
f6b7b42d75 | ||
|
|
da79f6cea1 | ||
|
|
d2888c62f1 | ||
|
|
dcbecf085f | ||
|
|
ae9fe506be | ||
|
|
70d9641e35 | ||
|
|
8ebfbccd08 | ||
|
|
b74b91b1c0 | ||
|
|
dd2adf4daa | ||
|
|
d791f3939e | ||
|
|
d9377fa50a | ||
|
|
f7e0386d1c | ||
|
|
1b8f0f2829 | ||
|
|
0f849e9bf1 | ||
|
|
f21559ea81 | ||
|
|
61174cfa17 | ||
|
|
8c073cedc6 | ||
|
|
361cffc283 | ||
|
|
8894400f5c | ||
|
|
b5f054d976 | ||
|
|
cb80dba234 | ||
|
|
9b7af0296e | ||
|
|
9d363da8f9 | ||
|
|
41a982fb31 | ||
|
|
7196fbc001 | ||
|
|
2f91fdd8b5 | ||
|
|
cb467b62c1 | ||
|
|
94ab57941d | ||
|
|
bed759333d | ||
|
|
2b76c87d1d | ||
|
|
7bbeeb7c61 | ||
|
|
58adb5e828 | ||
|
|
76ea92bfce | ||
|
|
ffa2870061 | ||
|
|
742ff027ba | ||
|
|
a29f2dd107 | ||
|
|
648c770cb4 | ||
|
|
f1c374403a | ||
|
|
5cbbe12b65 | ||
|
|
f58e3db924 | ||
|
|
0e57bee90d | ||
|
|
74855db87b | ||
|
|
8c6b34aaf2 | ||
|
|
cc2c65a9ab | ||
|
|
c27f178754 | ||
|
|
36f7ac3ff4 | ||
|
|
822c9aad22 | ||
|
|
1a83f2d344 | ||
|
|
28da88829e | ||
|
|
5453e1988c | ||
|
|
b09ca76333 | ||
|
|
12c1f0fa23 | ||
|
|
6a5b756cac | ||
|
|
aaf8a7fbfa | ||
|
|
2b829f9152 | ||
|
|
f84ca1d615 | ||
|
|
32e3396caf | ||
|
|
563187f3c0 | ||
|
|
05bf6b9b06 | ||
|
|
572c235b79 |
-22
@@ -1,22 +0,0 @@
|
|||||||
language: haskell
|
|
||||||
ghc: 7.8
|
|
||||||
addons:
|
|
||||||
postgresql: "9.3"
|
|
||||||
notifications:
|
|
||||||
slack: looprecur:z2nPS2Cfx2D33LE2obyU2FyQ
|
|
||||||
before_install:
|
|
||||||
- createuser --superuser --no-password dbapi_test
|
|
||||||
- createdb -O dbapi_test -U postgres dbapi_test
|
|
||||||
- travis_retry sudo add-apt-repository -y ppa:hvr/ghc
|
|
||||||
- travis_retry sudo apt-get update
|
|
||||||
- travis_retry sudo apt-get install --force-yes happy-1.19.3 alex-3.1.3
|
|
||||||
- export PATH=/opt/alex/3.1.3/bin:/opt/happy/1.19.3/bin:$PATH
|
|
||||||
install:
|
|
||||||
- travis_retry curl http://bin.begriffs.com/dbapi/cabal-sandbox.tar.xz | tar xJ
|
|
||||||
- chmod a+x .cabal-sandbox/bin/*
|
|
||||||
- cabal sandbox init
|
|
||||||
- cabal install --enable-test --dependencies-only
|
|
||||||
- cabal install --enable-test
|
|
||||||
script:
|
|
||||||
- cabal test --show-details=always --test-options="--color"
|
|
||||||
- .cabal-sandbox/bin/hlint src/*.hs test/**/*.hs
|
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
# 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.7.0] - 2015-03-03
|
||||||
|
### Added
|
||||||
|
- Server response logging
|
||||||
|
- Filter IN values, e.g. `?col=in.1,2,3`
|
||||||
|
- Return POSTed resource if header "Prefer: return=representation"
|
||||||
|
- Allow override of default (v1) schema
|
||||||
|
|
||||||
|
## [0.2.6.0] - 2015-02-18
|
||||||
|
### Added
|
||||||
|
- A changelog
|
||||||
|
- Filter by substring match, e.g. `?col=like.*hello*` (or ilike for
|
||||||
|
case insensitivity).
|
||||||
|
- Access-Control-Expose-Headers for CORS
|
||||||
|
|
||||||
|
### Fixed
|
||||||
|
- Make filter position match docs, e.g. `?order=col.asc` rather
|
||||||
|
than `?order=asc.col`.
|
||||||
@@ -1,43 +1,164 @@
|
|||||||
## Serve a RESTful API from any Postgres database
|

|
||||||
|
|
||||||
[](https://travis-ci.org/begriffs/dbapi)
|
[](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>
|
||||||
|
|
||||||
### Installation
|
PostgREST serves a fully RESTful API from any existing PostgreSQL
|
||||||
|
database. It provides a cleaner, more standards-compliant, faster
|
||||||
|
API than you are likely to write from scratch.
|
||||||
|
|
||||||
```sh
|
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](http://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
|
||||||
brew install postgres
|
|
||||||
cabal install -j --enable-tests
|
Try making requests to the live demo server with an HTTP client
|
||||||
|
such as [postman](http://www.getpostman.com/). The structure of the
|
||||||
|
demo database is defined by
|
||||||
|
[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
|
||||||
|
|
||||||
|
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.7.0.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.7.0.tar.xz)) and invoke like so:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
postgrest --db-host localhost --db-port 5432 \
|
||||||
|
--db-name my_db --db-user postgres \
|
||||||
|
--db-pass foobar --db-pool 200 \
|
||||||
|
--anonymous postgres --port 3000 \
|
||||||
|
--v1schema public
|
||||||
```
|
```
|
||||||
|
|
||||||
Example usage:
|
In production include the `--secure` option which redirects all
|
||||||
|
requests to HTTPS. Note that PostgREST does not handle the SSL
|
||||||
|
internally and must be put behind another server that does (such
|
||||||
|
as nginx or the Heroku load balancer).
|
||||||
|
|
||||||
```sh
|
### Performance
|
||||||
cabal run -p 3000 -d postgres://[auth-role]:@localhost:5432/[database] -a [anonymous-role]
|
|
||||||
```
|
|
||||||
|
|
||||||
You will need to provide two database roles (which are allowed to
|
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))
|
||||||
be the same). One is called the authenticator role (`auth-role`
|
|
||||||
above) which should have enough privileges to read the `auth` table
|
|
||||||
in the `dbapi` schema if you intend to support multi-user applications.
|
|
||||||
|
|
||||||
The other role is for anonymous access (`anonymous-role` above).
|
If you're used to servers written in interpreted languages (or named
|
||||||
Immediately upon acceping any unauthenticated HTTP connection dbapi
|
after precious gems), prepare to be pleasantly surprised by PostgREST
|
||||||
assumes this role in its queries to postgres. Give this role as
|
performance.
|
||||||
much or little permissions as you would like.
|
|
||||||
|
|
||||||
### Running tests
|
Three factors contribute to the speed. First the server is written
|
||||||
|
in [Haskell](https://new-www.haskell.org/) using the
|
||||||
|
[Warp](http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks)
|
||||||
|
HTTP server (aka a compiled language with lightweight threads).
|
||||||
|
Next it delegates as much calculation as possible to the database
|
||||||
|
including
|
||||||
|
|
||||||
```sh
|
* Serializing JSON responses directly in SQL
|
||||||
createuser --superuser --no-password dbapi_test
|
* Data validation
|
||||||
createdb -O dbapi_test -U postgres dbapi_test
|
* Authorization
|
||||||
|
* Combined row counting and retrieval
|
||||||
|
* Data post in single command (`returning *`)
|
||||||
|
|
||||||
cabal test --show-details=always --test-options="--color"
|
Finally it uses the database efficiently with the
|
||||||
```
|
[Hasql](https://nikita-volkov.github.io/hasql-benchmarks/) library
|
||||||
|
by
|
||||||
|
|
||||||
### Accessing server on localhost
|
* Reusing prepared statements
|
||||||
|
* Keeping a pool of db connections
|
||||||
|
* Using the Postgres binary protocol
|
||||||
|
* Being stateless to allow horizontal scaling
|
||||||
|
|
||||||
Dbapi permits only secure https access. By default it uses a
|
Ultimately the server (when load balanced) is constrained by database
|
||||||
self-signed key which will cause Chrome and other rest clients to
|
performance. This may make it inappropriate for very large traffic
|
||||||
complain. You will need to configure your system to trust this key.
|
load. To learn more about scaling with Heroku and Amazon RDS see
|
||||||
On mac follow [these
|
the [performance guide](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling).
|
||||||
instructions](http://www.robpeck.com/2010/10/google-chrome-mac-os-x-and-self-signed-ssl-certificates/).
|
|
||||||
|
Other optimizations are possible, and some are outlined in the
|
||||||
|
[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
|
||||||
|
|
||||||
|
A robust long-lived API needs the freedom to exist in multiple
|
||||||
|
versions. PostgREST supports versioning through HTTP content
|
||||||
|
negotiation. Requests for a certain version translate into switching
|
||||||
|
which database schema to search for tables. PostgreSQL schema search
|
||||||
|
paths allow tables from earlier versions to be reused verbatim in
|
||||||
|
later versions.
|
||||||
|
|
||||||
|
To learn more, see the [guide to versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning).
|
||||||
|
|
||||||
|
### Self-documention
|
||||||
|
|
||||||
|
Rather than writing and maintaining separate docs yourself let the
|
||||||
|
API explain its own affordances using HTTP. All PostgREST endpoints
|
||||||
|
respond to the OPTIONS verb and explain what they support as well
|
||||||
|
as the data format of their JSON payload.
|
||||||
|
|
||||||
|
The number of rows returned by an endpoint is reported by - and
|
||||||
|
limited with - range headers. More about
|
||||||
|
[that](http://begriffs.com/posts/2014-03-06-beyond-http-header-links.html).
|
||||||
|
|
||||||
|
There are more opportunities for self-documentation listed in [Future
|
||||||
|
Features](#future-features).
|
||||||
|
|
||||||
|
### Data Integrity
|
||||||
|
|
||||||
|
Rather than relying on an Object Relational Mapper and custom
|
||||||
|
imperative coding, this system requires you put declarative constraints
|
||||||
|
directly into your database. Hence no application can corrupt your
|
||||||
|
data (including your API server).
|
||||||
|
|
||||||
|
The PostgREST exposes HTTP interface with safeguards to prevent
|
||||||
|
surprises, such as enforcing idempotent PUT requests, and
|
||||||
|
|
||||||
|
See examples of [Postgres
|
||||||
|
constraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)
|
||||||
|
and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
|
||||||
|
|
||||||
|
### Future Features
|
||||||
|
|
||||||
|
* Watching endpoint changes with sockets and Postgres pubsub
|
||||||
|
* Specifying per-view HTTP caching
|
||||||
|
* Inferring good default caching policies from the Postgres stats collector
|
||||||
|
* Generating mock data for test clients
|
||||||
|
* Maintaining separate connection pools per role to avoid "set/reset
|
||||||
|
role" performance penalty
|
||||||
|
* Describe more relationships with Link headers
|
||||||
|
* Depending on accept headers, render OPTIONS as [RAML](http://raml.org/) or a
|
||||||
|
relational diagram
|
||||||
|
* Add two-legged auth with OAuth 1.0a(?)
|
||||||
|
* ... 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
|
||||||
|
|
||||||
|
* [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
|
||||||
|
|||||||
@@ -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.7.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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
+10
@@ -0,0 +1,10 @@
|
|||||||
|
machine:
|
||||||
|
pre:
|
||||||
|
- createuser --superuser --no-password postgrest_test
|
||||||
|
- 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
|
||||||
-69
@@ -1,69 +0,0 @@
|
|||||||
name: dbapi
|
|
||||||
version: 0.2.2.1
|
|
||||||
synopsis: The database is your api
|
|
||||||
license: MIT
|
|
||||||
license-file: LICENSE
|
|
||||||
author: Joe Nelson, Adam Baker
|
|
||||||
maintainer: cred+github@begriffs.com
|
|
||||||
category: Web
|
|
||||||
build-type: Simple
|
|
||||||
cabal-version: >=1.10
|
|
||||||
|
|
||||||
executable dbapi
|
|
||||||
main-is: Main.hs
|
|
||||||
ghc-options: -Wall -W -Werror
|
|
||||||
default-language: Haskell2010
|
|
||||||
build-depends: base >=4.6 && <5
|
|
||||||
, HDBC, HDBC-postgresql
|
|
||||||
, warp, wai >= 3.0.1 && < 3.0.2
|
|
||||||
, wai-extra, wai-cors
|
|
||||||
, wai-middleware-static >= 0.6.0
|
|
||||||
, HTTP, convertible, http-types
|
|
||||||
, case-insensitive
|
|
||||||
, scientific, time
|
|
||||||
, aeson, network >= 2.6
|
|
||||||
, bytestring, text, split, string-conversions
|
|
||||||
, containers, unordered-containers
|
|
||||||
, optparse-applicative >= 0.9.1 && < 0.10
|
|
||||||
, regex-base, regex-tdfa
|
|
||||||
, Ranged-sets
|
|
||||||
, transformers
|
|
||||||
, bcrypt, base64-string
|
|
||||||
, network-uri >= 2.6
|
|
||||||
, resource-pool, process
|
|
||||||
Other-Modules: Dbapi
|
|
||||||
, PgStructure
|
|
||||||
, PgQuery
|
|
||||||
, RangeQuery
|
|
||||||
, Middleware
|
|
||||||
hs-source-dirs: src
|
|
||||||
|
|
||||||
Test-Suite spec
|
|
||||||
Type: exitcode-stdio-1.0
|
|
||||||
Default-Language: Haskell2010
|
|
||||||
Hs-Source-Dirs: test, src
|
|
||||||
ghc-options: -Wall -W -Werror
|
|
||||||
Main-Is: Main.hs
|
|
||||||
Other-Modules: Dbapi, Spec, SpecHelper
|
|
||||||
Build-Depends: base, hspec2
|
|
||||||
, hspec-wai >= 0.5.0, hspec-wai-json
|
|
||||||
, HDBC, HDBC-postgresql
|
|
||||||
, warp, wai >= 3.0.1 && < 3.0.2
|
|
||||||
, HTTP, convertible
|
|
||||||
, case-insensitive
|
|
||||||
, wai-extra, wai-cors, containers
|
|
||||||
, wai-middleware-static >= 0.6.0
|
|
||||||
, http-types, scientific, time
|
|
||||||
, bytestring, aeson, network >= 2.6
|
|
||||||
, text, optparse-applicative
|
|
||||||
, unordered-containers
|
|
||||||
, regex-base
|
|
||||||
, string-conversions
|
|
||||||
, http-media, regex-tdfa
|
|
||||||
, Ranged-sets
|
|
||||||
, transformers
|
|
||||||
, bcrypt
|
|
||||||
, base64-string
|
|
||||||
, split
|
|
||||||
, network-uri >= 2.6
|
|
||||||
, resource-pool
|
|
||||||
+101
@@ -0,0 +1,101 @@
|
|||||||
|
name: postgrest
|
||||||
|
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.7.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
|
||||||
|
cabal-version: >=1.10
|
||||||
|
|
||||||
|
executable postgrest
|
||||||
|
main-is: Main.hs
|
||||||
|
ghc-options: -Wall -W -O2
|
||||||
|
default-language: Haskell2010
|
||||||
|
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||||
|
build-depends: base >=4.6 && <5
|
||||||
|
, 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
|
||||||
|
, HTTP, convertible, http-types
|
||||||
|
, case-insensitive
|
||||||
|
, scientific, time
|
||||||
|
, aeson, network >= 2.6
|
||||||
|
, bytestring, text, split, string-conversions
|
||||||
|
, stringsearch
|
||||||
|
, containers, unordered-containers
|
||||||
|
, optparse-applicative == 0.11.*
|
||||||
|
, regex-base, regex-tdfa
|
||||||
|
, regex-tdfa-text
|
||||||
|
, Ranged-sets
|
||||||
|
, transformers, MissingH
|
||||||
|
, bcrypt >= 0.0.6, base64-string
|
||||||
|
, network-uri >= 2.6
|
||||||
|
, resource-pool
|
||||||
|
, blaze-builder
|
||||||
|
, vector
|
||||||
|
, mtl
|
||||||
|
Other-Modules: App
|
||||||
|
, Auth
|
||||||
|
, Config
|
||||||
|
, Error
|
||||||
|
, Middleware
|
||||||
|
, PgQuery
|
||||||
|
, PgStructure
|
||||||
|
, RangeQuery
|
||||||
|
, Types
|
||||||
|
hs-source-dirs: src
|
||||||
|
|
||||||
|
Test-Suite spec
|
||||||
|
Type: exitcode-stdio-1.0
|
||||||
|
Default-Language: Haskell2010
|
||||||
|
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
|
||||||
|
Hs-Source-Dirs: test, src
|
||||||
|
ghc-options: -Wall -W -Werror
|
||||||
|
Main-Is: Main.hs
|
||||||
|
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, hasql-backend
|
||||||
|
, hasql-postgres
|
||||||
|
, warp, wai
|
||||||
|
, packdeps, hlint
|
||||||
|
, HTTP, convertible
|
||||||
|
, case-insensitive
|
||||||
|
, wai-extra, wai-cors, containers
|
||||||
|
, wai-middleware-static
|
||||||
|
, http-types, scientific, time
|
||||||
|
, bytestring, aeson, network
|
||||||
|
, text, optparse-applicative
|
||||||
|
, stringsearch
|
||||||
|
, unordered-containers
|
||||||
|
, regex-base
|
||||||
|
, string-conversions
|
||||||
|
, http-media, regex-tdfa
|
||||||
|
, regex-tdfa-text
|
||||||
|
, Ranged-sets
|
||||||
|
, transformers, MissingH, split
|
||||||
|
, bcrypt, base64-string
|
||||||
|
, network-uri
|
||||||
|
, resource-pool
|
||||||
|
, blaze-builder
|
||||||
|
, vector
|
||||||
|
, mtl
|
||||||
|
, process
|
||||||
Executable
+10
@@ -0,0 +1,10 @@
|
|||||||
|
export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'`
|
||||||
|
|
||||||
|
curl -L http://softlayer-ams.dl.sourceforge.net/project/s3tools/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
|
||||||
|
|
||||||
|
cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER}
|
||||||
|
tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER}
|
||||||
|
|
||||||
|
touch ~/.s3cfg
|
||||||
|
|
||||||
|
s3cmd-1.5.0-alpha1/s3cmd put --access_key=${S3_ACCESS_KEY} --secret_key=${S3_SECRET_KEY} -P -f postgrest-${POSTGREST_VER}.tar.xz $S3_BUCKET/postgrest-${POSTGREST_VER}.tar.xz
|
||||||
+239
@@ -0,0 +1,239 @@
|
|||||||
|
{-# LANGUAGE FlexibleContexts #-}
|
||||||
|
module App (app, sqlError, isSqlError) where
|
||||||
|
|
||||||
|
import Control.Monad (join)
|
||||||
|
import Control.Arrow ((***))
|
||||||
|
import Control.Applicative
|
||||||
|
|
||||||
|
import Data.Text hiding (map)
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import Text.Regex.TDFA ((=~))
|
||||||
|
import Data.Ord (comparing)
|
||||||
|
import Data.Ranged.Ranges (emptyRange)
|
||||||
|
import Data.HashMap.Strict (keys, elems, filterWithKey, toList)
|
||||||
|
import Data.String.Conversions (cs)
|
||||||
|
import Data.List (sortBy)
|
||||||
|
import Data.Functor.Identity
|
||||||
|
import qualified Data.Set as S
|
||||||
|
import qualified Data.ByteString.Lazy as BL
|
||||||
|
|
||||||
|
import Network.HTTP.Types.Status
|
||||||
|
import Network.HTTP.Types.Header
|
||||||
|
import Network.HTTP.Types.URI (parseSimpleQuery)
|
||||||
|
import Network.HTTP.Base (urlEncodeVars)
|
||||||
|
import Network.Wai
|
||||||
|
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Monoid
|
||||||
|
import qualified Data.Vector as V
|
||||||
|
import qualified Hasql as H
|
||||||
|
import qualified Hasql.Backend as B
|
||||||
|
import qualified Hasql.Postgres as P
|
||||||
|
|
||||||
|
import Auth
|
||||||
|
import PgQuery
|
||||||
|
import RangeQuery
|
||||||
|
import PgStructure
|
||||||
|
|
||||||
|
app :: Text -> BL.ByteString -> Request -> H.Tx P.Postgres s Response
|
||||||
|
app v1schema reqBody req =
|
||||||
|
case (path, verb) of
|
||||||
|
([], _) -> do
|
||||||
|
body <- encode <$> tables (cs schema)
|
||||||
|
return $ responseLBS status200 [jsonH] $ cs body
|
||||||
|
|
||||||
|
([table], "OPTIONS") -> do
|
||||||
|
let t = QualifiedTable schema (cs table)
|
||||||
|
cols <- columns t
|
||||||
|
pkey <- map cs <$> primaryKeyColumns t
|
||||||
|
return $ responseLBS status200 [jsonH, allOrigins]
|
||||||
|
$ encode (TableOptions cols pkey)
|
||||||
|
|
||||||
|
([table], "GET") ->
|
||||||
|
if range == Just emptyRange
|
||||||
|
then return $ responseLBS status416 [] "HTTP Range error"
|
||||||
|
else do
|
||||||
|
let qt = QualifiedTable schema (cs table)
|
||||||
|
let select = B.Stmt "select " V.empty True <>
|
||||||
|
parentheticT (
|
||||||
|
whereT qq $ countRows qt
|
||||||
|
) <> commaq <> (
|
||||||
|
asJsonWithCount
|
||||||
|
. limitT range
|
||||||
|
. orderT (orderParse qq)
|
||||||
|
. whereT qq
|
||||||
|
$ selectStar qt
|
||||||
|
)
|
||||||
|
row <- H.maybeEx select
|
||||||
|
let (tableTotal, queryTotal, body) =
|
||||||
|
fromMaybe (0, 0, Just "" :: Maybe Text) row
|
||||||
|
from = fromMaybe 0 $ rangeOffset <$> range
|
||||||
|
to = from+queryTotal-1
|
||||||
|
contentRange = contentRangeH from to tableTotal
|
||||||
|
status = rangeStatus from to tableTotal
|
||||||
|
canonical = urlEncodeVars
|
||||||
|
. sortBy (comparing fst)
|
||||||
|
. map (join (***) cs)
|
||||||
|
. parseSimpleQuery
|
||||||
|
$ rawQueryString req
|
||||||
|
return $ responseLBS status
|
||||||
|
[jsonH, contentRange,
|
||||||
|
("Content-Location",
|
||||||
|
"/" <> cs table <>
|
||||||
|
if Prelude.null canonical then "" else "?" <> cs canonical
|
||||||
|
)
|
||||||
|
] (cs $ fromMaybe "[]" body)
|
||||||
|
|
||||||
|
(["postgrest", "users"], "POST") -> do
|
||||||
|
let user = decode reqBody :: Maybe AuthUser
|
||||||
|
|
||||||
|
case user of
|
||||||
|
Nothing -> return $ responseLBS status400 [jsonH] $
|
||||||
|
encode . object $ [("message", String "Failed to parse user.")]
|
||||||
|
Just u -> do
|
||||||
|
_ <- addUser (cs $ userId u)
|
||||||
|
(cs $ userPass u) (cs $ userRole u)
|
||||||
|
return $ responseLBS status201
|
||||||
|
[ jsonH
|
||||||
|
, (hLocation, "/postgrest/users?id=eq." <> cs (userId u))
|
||||||
|
] ""
|
||||||
|
|
||||||
|
([table], "POST") ->
|
||||||
|
handleJsonObj reqBody $ \obj -> do
|
||||||
|
let qt = QualifiedTable schema (cs table)
|
||||||
|
query = insertInto qt (map cs $ keys obj) (elems obj)
|
||||||
|
echoRequested = lookup "Prefer" hdrs == Just "return=representation"
|
||||||
|
row <- H.maybeEx query
|
||||||
|
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
|
||||||
|
Just inserted = decode (cs insertedJson) :: Maybe Object
|
||||||
|
|
||||||
|
primaryKeys <- map cs <$> primaryKeyColumns qt
|
||||||
|
let primaries = if Prelude.null primaryKeys
|
||||||
|
then inserted
|
||||||
|
else filterWithKey (const . (`elem` primaryKeys)) inserted
|
||||||
|
let params = urlEncodeVars
|
||||||
|
$ map (\t -> (cs $ fst t, "eq." <> cs (unquoted $ snd t)))
|
||||||
|
$ sortBy (comparing fst) $ toList primaries
|
||||||
|
return $ responseLBS status201
|
||||||
|
[ jsonH
|
||||||
|
, (hLocation, "/" <> cs table <> "?" <> cs params)
|
||||||
|
] $ if echoRequested then cs insertedJson else ""
|
||||||
|
|
||||||
|
([table], "PUT") ->
|
||||||
|
handleJsonObj reqBody $ \obj -> do
|
||||||
|
let qt = QualifiedTable schema (cs table)
|
||||||
|
primaryKeys <- primaryKeyColumns qt
|
||||||
|
let specifiedKeys = map (cs . fst) qq
|
||||||
|
if S.fromList primaryKeys /= S.fromList specifiedKeys
|
||||||
|
then return $ responseLBS status405 []
|
||||||
|
"You must speficy all and only primary keys as params"
|
||||||
|
else do
|
||||||
|
tableCols <- map (cs . colName) <$> columns qt
|
||||||
|
let cols = map cs $ keys obj
|
||||||
|
if S.fromList tableCols == S.fromList cols
|
||||||
|
then do
|
||||||
|
let vals = elems obj
|
||||||
|
H.unitEx $ iffNotT
|
||||||
|
(whereT qq $ update qt cols vals)
|
||||||
|
(insertSelect qt cols vals)
|
||||||
|
return $ responseLBS status204 [ jsonH ] ""
|
||||||
|
|
||||||
|
else return $ if Prelude.null tableCols
|
||||||
|
then responseLBS status404 [] ""
|
||||||
|
else responseLBS status400 []
|
||||||
|
"You must specify all columns in PUT request"
|
||||||
|
|
||||||
|
([table], "PATCH") ->
|
||||||
|
handleJsonObj reqBody $ \obj -> do
|
||||||
|
let qt = QualifiedTable schema (cs table)
|
||||||
|
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 [] ""
|
||||||
|
|
||||||
|
where
|
||||||
|
path = pathInfo req
|
||||||
|
verb = requestMethod req
|
||||||
|
qq = queryString req
|
||||||
|
hdrs = requestHeaders req
|
||||||
|
schema = requestedSchema v1schema hdrs
|
||||||
|
range = rangeRequested hdrs
|
||||||
|
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
||||||
|
|
||||||
|
sqlError :: t
|
||||||
|
sqlError = undefined
|
||||||
|
|
||||||
|
isSqlError :: t
|
||||||
|
isSqlError = undefined
|
||||||
|
|
||||||
|
rangeStatus :: Int -> Int -> Int -> Status
|
||||||
|
rangeStatus from to total
|
||||||
|
| from > total = status416
|
||||||
|
| (1 + to - from) < total = status206
|
||||||
|
| otherwise = status200
|
||||||
|
|
||||||
|
contentRangeH :: Int -> Int -> Int -> Header
|
||||||
|
contentRangeH from to total =
|
||||||
|
("Content-Range",
|
||||||
|
if total == 0 || from > total
|
||||||
|
then "*/" <> cs (show total)
|
||||||
|
else cs (show from) <> "-"
|
||||||
|
<> cs (show to) <> "/"
|
||||||
|
<> cs (show total)
|
||||||
|
)
|
||||||
|
|
||||||
|
requestedSchema :: Text -> RequestHeaders -> Text
|
||||||
|
requestedSchema v1schema hdrs =
|
||||||
|
case verStr of
|
||||||
|
Just [[_, ver]] -> if ver == "1" then v1schema else ver
|
||||||
|
_ -> v1schema
|
||||||
|
|
||||||
|
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
|
||||||
|
accept = cs <$> lookup hAccept hdrs :: Maybe Text
|
||||||
|
verStr = (=~ verRegex) <$> accept :: Maybe [[Text]]
|
||||||
|
|
||||||
|
jsonH :: Header
|
||||||
|
jsonH = (hContentType, "application/json")
|
||||||
|
|
||||||
|
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
|
||||||
|
Left err ->
|
||||||
|
return $ responseLBS status400 [jsonH] jErr
|
||||||
|
where
|
||||||
|
jErr = encode . object $
|
||||||
|
[("message", String $ "Failed to parse JSON payload. " <> cs err)]
|
||||||
|
Right (Object o) -> handler o
|
||||||
|
Right _ ->
|
||||||
|
return $ responseLBS status400 [jsonH] jErr
|
||||||
|
where
|
||||||
|
jErr = encode . object $
|
||||||
|
[("message", String "Expecting a JSON object")]
|
||||||
|
|
||||||
|
|
||||||
|
data TableOptions = TableOptions {
|
||||||
|
tblOptcolumns :: [Column]
|
||||||
|
, tblOptpkey :: [Text]
|
||||||
|
}
|
||||||
|
|
||||||
|
instance ToJSON TableOptions where
|
||||||
|
toJSON t = object [
|
||||||
|
"columns" .= tblOptcolumns t
|
||||||
|
, "pkey" .= tblOptpkey t ]
|
||||||
+71
@@ -0,0 +1,71 @@
|
|||||||
|
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables, OverloadedStrings #-}
|
||||||
|
module Auth where
|
||||||
|
|
||||||
|
import Data.Aeson
|
||||||
|
import Control.Monad (mzero)
|
||||||
|
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.Backend as B
|
||||||
|
import qualified Hasql.Postgres as P
|
||||||
|
import Data.String.Conversions (cs)
|
||||||
|
import PgQuery (pgFmtLit)
|
||||||
|
|
||||||
|
import System.IO.Unsafe
|
||||||
|
|
||||||
|
data AuthUser = AuthUser {
|
||||||
|
userId :: String
|
||||||
|
, userPass :: String
|
||||||
|
, userRole :: String
|
||||||
|
} deriving (Show)
|
||||||
|
|
||||||
|
instance FromJSON AuthUser where
|
||||||
|
parseJSON (Object v) = AuthUser <$>
|
||||||
|
v .: "id" <*>
|
||||||
|
v .: "pass" <*>
|
||||||
|
v .: "role"
|
||||||
|
parseJSON _ = mzero
|
||||||
|
|
||||||
|
instance ToJSON AuthUser where
|
||||||
|
toJSON u = object [
|
||||||
|
"id" .= userId u
|
||||||
|
, "pass" .= userPass u
|
||||||
|
, "role" .= userRole u ]
|
||||||
|
|
||||||
|
type DbRole = Text
|
||||||
|
|
||||||
|
data LoginAttempt =
|
||||||
|
NoCredentials
|
||||||
|
| MalformedAuth
|
||||||
|
| LoginFailed
|
||||||
|
| LoginSuccess DbRole
|
||||||
|
deriving (Eq, Show)
|
||||||
|
|
||||||
|
checkPass :: Text -> Text -> Bool
|
||||||
|
checkPass = (. cs) . validatePassword . cs
|
||||||
|
|
||||||
|
setRole :: Text -> H.Tx P.Postgres s ()
|
||||||
|
setRole role = H.unitEx $ B.Stmt ("set role " <> cs (pgFmtLit role)) V.empty True
|
||||||
|
|
||||||
|
resetRole :: H.Tx P.Postgres s ()
|
||||||
|
resetRole = H.unitEx [H.stmt|reset role|]
|
||||||
|
|
||||||
|
addUser :: Text -> Text -> Text -> H.Tx P.Postgres s ()
|
||||||
|
addUser identity pass role = do
|
||||||
|
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
|
||||||
|
H.unitEx $
|
||||||
|
[H.stmt|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
|
||||||
|
identity (cs hashed :: Text) role
|
||||||
|
|
||||||
|
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
|
||||||
|
signInRole user pass = do
|
||||||
|
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
|
||||||
|
then LoginSuccess role
|
||||||
|
else LoginFailed
|
||||||
|
) u
|
||||||
@@ -0,0 +1,60 @@
|
|||||||
|
module Config where
|
||||||
|
|
||||||
|
import Network.Wai
|
||||||
|
import Control.Applicative
|
||||||
|
import Data.Text (strip)
|
||||||
|
import qualified Data.CaseInsensitive as CI
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
import Data.String.Conversions (cs)
|
||||||
|
import Options.Applicative hiding (columns)
|
||||||
|
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..))
|
||||||
|
|
||||||
|
data AppConfig = AppConfig {
|
||||||
|
configDbName :: String
|
||||||
|
, configDbPort :: Int
|
||||||
|
, configDbUser :: String
|
||||||
|
, configDbPass :: String
|
||||||
|
, configDbHost :: String
|
||||||
|
|
||||||
|
, configPort :: Int
|
||||||
|
, configAnonRole :: String
|
||||||
|
, configSecure :: Bool
|
||||||
|
, configPool :: Int
|
||||||
|
, configV1Schema :: String
|
||||||
|
}
|
||||||
|
|
||||||
|
argParser :: Parser AppConfig
|
||||||
|
argParser = AppConfig
|
||||||
|
<$> 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 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)
|
||||||
|
<*> strOption (long "v1schema" <> metavar "NAME" <> value "1" <> help "Schema to use for nonspecified version (or explicit v1)" <> showDefault)
|
||||||
|
|
||||||
|
defaultCorsPolicy :: CorsResourcePolicy
|
||||||
|
defaultCorsPolicy = CorsResourcePolicy Nothing
|
||||||
|
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
||||||
|
(Just $ 60*60*24) False False True
|
||||||
|
|
||||||
|
corsPolicy :: Request -> Maybe CorsResourcePolicy
|
||||||
|
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
|
||||||
|
headers = requestHeaders req
|
||||||
|
accHeaders = case lookup "access-control-request-headers" headers of
|
||||||
|
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
|
||||||
|
Nothing -> []
|
||||||
-204
@@ -1,204 +0,0 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
-- {{{ Imports
|
|
||||||
module Dbapi where
|
|
||||||
|
|
||||||
import Types (SqlRow, getRow)
|
|
||||||
|
|
||||||
import Control.Monad (join)
|
|
||||||
import Control.Arrow ((***))
|
|
||||||
import Control.Applicative
|
|
||||||
import Options.Applicative hiding (columns)
|
|
||||||
|
|
||||||
import Data.Maybe (fromMaybe, isJust)
|
|
||||||
import Text.Regex.TDFA ((=~))
|
|
||||||
import Data.Map (intersection, fromList, toList, Map)
|
|
||||||
import Data.List (sort)
|
|
||||||
import qualified Data.Set as S
|
|
||||||
import Data.Convertible.Base (convert)
|
|
||||||
import Data.Text (strip)
|
|
||||||
|
|
||||||
import Network.HTTP.Types.Status
|
|
||||||
import Network.HTTP.Types.Header
|
|
||||||
import Network.HTTP.Types.URI
|
|
||||||
|
|
||||||
import Network.HTTP.Base (urlEncodeVars)
|
|
||||||
|
|
||||||
import Network.Wai
|
|
||||||
import Network.Wai.Internal
|
|
||||||
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..))
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
|
||||||
import Data.String.Conversions (cs)
|
|
||||||
import qualified Data.CaseInsensitive as CI
|
|
||||||
|
|
||||||
import Database.HDBC.PostgreSQL (Connection)
|
|
||||||
import PgStructure (printTables, printColumns, primaryKeyColumns,
|
|
||||||
columns, Column(colName))
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
|
|
||||||
import PgQuery
|
|
||||||
import RangeQuery
|
|
||||||
import Data.Ranged.Ranges (emptyRange)
|
|
||||||
|
|
||||||
-- }}}
|
|
||||||
|
|
||||||
data AppConfig = AppConfig {
|
|
||||||
configDbUri :: String
|
|
||||||
, configPort :: Int
|
|
||||||
, configAnonRole :: String
|
|
||||||
, configSecure :: Bool
|
|
||||||
}
|
|
||||||
|
|
||||||
jsonContentType :: (HeaderName, BS.ByteString)
|
|
||||||
jsonContentType = (hContentType, "application/json")
|
|
||||||
|
|
||||||
jsonBodyAction :: Request -> (SqlRow -> IO Response) -> IO Response
|
|
||||||
jsonBodyAction req handler = do
|
|
||||||
parse <- jsonBody req
|
|
||||||
case parse of
|
|
||||||
Left err -> return $ responseLBS status400 [jsonContentType] json
|
|
||||||
where json = JSON.encode . JSON.object $ [("error", JSON.String $ "Failed to parse JSON payload. " <> cs err) ]
|
|
||||||
Right body -> handler body
|
|
||||||
|
|
||||||
jsonBody :: Request -> IO (Either String SqlRow)
|
|
||||||
jsonBody = fmap JSON.eitherDecode . strictRequestBody
|
|
||||||
|
|
||||||
filterByKeys :: Ord a => Map a b -> [a] -> Map a b
|
|
||||||
filterByKeys m keys =
|
|
||||||
if null keys then m else
|
|
||||||
m `intersection` fromList (zip keys $ repeat undefined)
|
|
||||||
|
|
||||||
app :: Connection -> Application
|
|
||||||
app conn req respond =
|
|
||||||
respond =<< case (path, verb) of
|
|
||||||
([], _) ->
|
|
||||||
responseLBS status200 [jsonContentType] <$> printTables ver conn
|
|
||||||
|
|
||||||
([table], "OPTIONS") ->
|
|
||||||
responseLBS status200 [jsonContentType, allOrigins] <$>
|
|
||||||
printColumns ver (cs table) conn
|
|
||||||
|
|
||||||
([table], "GET") ->
|
|
||||||
if range == Just emptyRange
|
|
||||||
then return $ responseLBS status416 [] "HTTP Range error"
|
|
||||||
else do
|
|
||||||
r <- respondWithRangedResult <$> getRows ver (cs table) qq range conn
|
|
||||||
let canonical = urlEncodeVars $ sort $
|
|
||||||
map (join (***) cs) $
|
|
||||||
parseSimpleQuery $
|
|
||||||
rawQueryString req
|
|
||||||
return $ addHeaders [
|
|
||||||
("Content-Location",
|
|
||||||
"/" <> cs table <> if null canonical then "" else "?" <> cs canonical
|
|
||||||
)] r
|
|
||||||
|
|
||||||
([table], "POST") ->
|
|
||||||
jsonBodyAction req (\row -> do
|
|
||||||
allvals <- insert ver table row conn
|
|
||||||
keys <- primaryKeyColumns ver (cs table) conn
|
|
||||||
let params = urlEncodeVars $ map (\t -> (fst t, "eq." <> convert (snd t) :: String)) $ toList $ filterByKeys allvals keys
|
|
||||||
return $ responseLBS status201
|
|
||||||
[ jsonContentType
|
|
||||||
, (hLocation, "/" <> cs table <> "?" <> cs params)
|
|
||||||
] ""
|
|
||||||
)
|
|
||||||
|
|
||||||
([table], "PUT") ->
|
|
||||||
jsonBodyAction req (\row -> do
|
|
||||||
keys <- primaryKeyColumns ver (cs table) conn
|
|
||||||
let specifiedKeys = map (cs . fst) qq
|
|
||||||
if S.fromList keys /= S.fromList specifiedKeys
|
|
||||||
then return $ responseLBS status405 []
|
|
||||||
"You must speficy all and only primary keys as params"
|
|
||||||
else
|
|
||||||
if isJust cRange
|
|
||||||
then return $ responseLBS status400 []
|
|
||||||
"Content-Range is not allowed in PUT request"
|
|
||||||
else do
|
|
||||||
cols <- columns ver (cs table) conn
|
|
||||||
let colNames = S.fromList $ map (cs . colName) cols
|
|
||||||
let specifiedCols = S.fromList $ map fst $ getRow row
|
|
||||||
return $ if colNames == specifiedCols then
|
|
||||||
responseLBS status200 [ jsonContentType ] ""
|
|
||||||
|
|
||||||
else if S.null colNames then responseLBS status404 [] ""
|
|
||||||
else responseLBS status400 []
|
|
||||||
"You must specify all columns in PUT request"
|
|
||||||
)
|
|
||||||
|
|
||||||
(_, _) ->
|
|
||||||
return $ responseLBS status404 [] ""
|
|
||||||
|
|
||||||
where
|
|
||||||
path = pathInfo req
|
|
||||||
verb = requestMethod req
|
|
||||||
qq = queryString req
|
|
||||||
hdrs = requestHeaders req
|
|
||||||
ver = fromMaybe "1" $ requestedVersion hdrs
|
|
||||||
range = requestedRange hdrs
|
|
||||||
cRange = requestedContentRange hdrs
|
|
||||||
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
|
|
||||||
|
|
||||||
defaultCorsPolicy :: CorsResourcePolicy
|
|
||||||
defaultCorsPolicy = CorsResourcePolicy Nothing
|
|
||||||
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
|
|
||||||
(Just $ 60*60*24) False False True
|
|
||||||
|
|
||||||
corsPolicy :: Request -> Maybe CorsResourcePolicy
|
|
||||||
corsPolicy req = case lookup "origin" headers of
|
|
||||||
Just origin -> Just defaultCorsPolicy {
|
|
||||||
corsOrigins = Just ([origin], True)
|
|
||||||
, corsRequestHeaders = "Authentication":accHeaders
|
|
||||||
}
|
|
||||||
Nothing -> Nothing
|
|
||||||
where
|
|
||||||
headers = requestHeaders req
|
|
||||||
accHeaders = case lookup "access-control-request-headers" headers of
|
|
||||||
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
|
|
||||||
Nothing -> []
|
|
||||||
|
|
||||||
|
|
||||||
respondWithRangedResult :: RangedResult -> Response
|
|
||||||
respondWithRangedResult rr =
|
|
||||||
responseLBS status [
|
|
||||||
jsonContentType,
|
|
||||||
("Content-Range",
|
|
||||||
if total == 0 || from > total
|
|
||||||
then "*/" <> cs (show total)
|
|
||||||
else cs (show from) <> "-"
|
|
||||||
<> cs (show to) <> "/"
|
|
||||||
<> cs (show total)
|
|
||||||
)
|
|
||||||
] (rrBody rr)
|
|
||||||
|
|
||||||
where
|
|
||||||
from = rrFrom rr
|
|
||||||
to = rrTo rr
|
|
||||||
total = rrTotal rr
|
|
||||||
status
|
|
||||||
| from > total = status416
|
|
||||||
| (1 + to - from) < total = status206
|
|
||||||
| otherwise = status200
|
|
||||||
|
|
||||||
requestedVersion :: RequestHeaders -> Maybe String
|
|
||||||
requestedVersion hdrs =
|
|
||||||
case verStr of
|
|
||||||
Just [[_, ver]] -> Just ver
|
|
||||||
_ -> Nothing
|
|
||||||
|
|
||||||
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
|
|
||||||
accept = cs <$> lookup hAccept hdrs :: Maybe String
|
|
||||||
verStr = (=~ verRegex) <$> accept :: Maybe [[String]]
|
|
||||||
|
|
||||||
|
|
||||||
addHeaders :: ResponseHeaders -> Response -> Response
|
|
||||||
addHeaders hdrs (ResponseFile s headers fp m) =
|
|
||||||
ResponseFile s (headers ++ hdrs) fp m
|
|
||||||
addHeaders hdrs (ResponseBuilder s headers b) =
|
|
||||||
ResponseBuilder s (headers ++ hdrs) b
|
|
||||||
addHeaders hdrs (ResponseStream s headers b) =
|
|
||||||
ResponseStream s (headers ++ hdrs) b
|
|
||||||
addHeaders hdrs (ResponseRaw s resp) =
|
|
||||||
ResponseRaw s (addHeaders hdrs resp)
|
|
||||||
@@ -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
|
||||||
+55
-32
@@ -1,47 +1,70 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Main where
|
module Main where
|
||||||
import Dbapi
|
|
||||||
import Middleware (inTransaction, authenticated, withSavepoint, clientErrors,
|
import Paths_postgrest (version)
|
||||||
redirectInsecure, withDBConnection)
|
|
||||||
import Network.Wai.Handler.Warp hiding (Connection)
|
import App
|
||||||
import Data.String.Conversions (cs)
|
import Middleware
|
||||||
|
import Error(errResponse)
|
||||||
|
|
||||||
import Control.Monad (unless)
|
import Control.Monad (unless)
|
||||||
import Control.Applicative
|
import Control.Monad.IO.Class (liftIO)
|
||||||
import Options.Applicative hiding (columns)
|
import Data.String.Conversions (cs)
|
||||||
import Network.Wai.Middleware.Gzip (gzip, def)
|
import Network.Wai (strictRequestBody)
|
||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
|
import Network.Wai.Handler.Warp hiding (Connection)
|
||||||
|
import Network.Wai.Middleware.Gzip (gzip, def)
|
||||||
import Network.Wai.Middleware.Static (staticPolicy, only)
|
import Network.Wai.Middleware.Static (staticPolicy, only)
|
||||||
import Database.HDBC (disconnect)
|
import Network.Wai.Middleware.RequestLogger (logStdout)
|
||||||
import Database.HDBC.PostgreSQL(connectPostgreSQL')
|
import Data.List (intercalate)
|
||||||
import Data.Pool(createPool)
|
import Data.Version (versionBranch)
|
||||||
|
import qualified Hasql as H
|
||||||
|
import qualified Hasql.Postgres as P
|
||||||
|
import Options.Applicative hiding (columns)
|
||||||
|
|
||||||
argParser :: Parser AppConfig
|
import Config (AppConfig(..), argParser, corsPolicy)
|
||||||
argParser = AppConfig
|
|
||||||
<$> strOption (long "db" <> short 'd' <> metavar "URI"
|
|
||||||
<> help "database uri to expose, e.g. postgres://user:pass@host:port/database")
|
|
||||||
<*> option (long "port" <> short 'p' <> metavar "NUMBER" <> value 3000
|
|
||||||
<> help "port number on which to run HTTP server")
|
|
||||||
<*> 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" )
|
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = do
|
||||||
conf <- execParser (info (helper <*> argParser) describe)
|
let opts = info (helper <*> argParser) $
|
||||||
pool <- createPool (connectPostgreSQL' (configDbUri conf)) disconnect 1 600 10
|
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) $
|
||||||
putStrLn "WARNING, running in insecure mode, auth will be in plaintext"
|
putStrLn "WARNING, running in insecure mode, auth will be in plaintext"
|
||||||
|
Prelude.putStrLn $ "Listening on port " ++
|
||||||
|
(show $ configPort conf :: String)
|
||||||
|
|
||||||
|
let pgSettings = P.ParamSettings (cs $ configDbHost conf)
|
||||||
|
(fromIntegral $ configDbPort conf)
|
||||||
|
(cs $ configDbUser conf)
|
||||||
|
(cs $ configDbPass conf)
|
||||||
|
(cs $ configDbName conf)
|
||||||
|
appSettings = setPort port
|
||||||
|
. setServerName (cs $ "postgrest/" <> prettyVersion)
|
||||||
|
$ defaultSettings
|
||||||
|
middle = logStdout
|
||||||
|
. (if configSecure conf then redirectInsecure else id)
|
||||||
|
. gzip def . cors corsPolicy
|
||||||
|
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
||||||
|
anonRole = cs $ configAnonRole conf
|
||||||
|
currRole = cs $ configDbUser conf
|
||||||
|
|
||||||
|
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 (cs $ configV1Schema conf) body) req
|
||||||
|
either (respond . errResponse) respond resOrError
|
||||||
|
|
||||||
Prelude.putStrLn $ "Listening on port " ++ (show $ configPort conf :: String)
|
|
||||||
run port $ (if configSecure conf then redirectInsecure else id)
|
|
||||||
. gzip def . cors corsPolicy . clientErrors
|
|
||||||
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
|
|
||||||
. withDBConnection pool . inTransaction
|
|
||||||
. authenticated (cs $ configAnonRole conf) . withSavepoint $ app
|
|
||||||
where
|
where
|
||||||
describe = progDesc "create a REST API to an existing Postgres database"
|
prettyVersion = intercalate "." $ map show $ versionBranch version
|
||||||
|
|||||||
+28
-62
@@ -1,91 +1,57 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
{-# LANGUAGE ScopedTypeVariables #-}
|
||||||
|
|
||||||
module Middleware where
|
module Middleware where
|
||||||
|
|
||||||
import Data.Aeson ((.=), toJSON, ToJSON, object, encode)
|
|
||||||
import Data.Maybe (fromMaybe)
|
import Data.Maybe (fromMaybe)
|
||||||
import Data.Monoid (mconcat)
|
import Data.Monoid (mconcat)
|
||||||
import Data.Pool(withResource, Pool)
|
import Data.Text
|
||||||
|
-- import Data.Pool(withResource, Pool)
|
||||||
import Database.HDBC (runRaw)
|
|
||||||
import Database.HDBC.PostgreSQL (Connection)
|
|
||||||
import Database.HDBC.Types (SqlError(..))
|
|
||||||
|
|
||||||
|
import qualified Hasql as H
|
||||||
|
import qualified Hasql.Postgres as P
|
||||||
import Data.String.Conversions(cs)
|
import Data.String.Conversions(cs)
|
||||||
import qualified Data.ByteString.Char8 as BS
|
|
||||||
import Control.Exception (finally, throw, catchJust, catch, SomeException,
|
|
||||||
bracket_)
|
|
||||||
|
|
||||||
import Network.HTTP.Types.Header (RequestHeaders, hContentType, hAuthorization,
|
import Network.HTTP.Types.Header (hLocation, hAuthorization)
|
||||||
hLocation)
|
import Network.HTTP.Types (RequestHeaders)
|
||||||
import Network.HTTP.Types.Status (status400, status401, status404, status301)
|
import Network.HTTP.Types.Status (status400, status401, status301)
|
||||||
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
|
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
|
||||||
rawQueryString, isSecure)
|
rawQueryString, isSecure, Request(..), Response)
|
||||||
import Network.URI (URI(..), parseURI)
|
import Network.URI (URI(..), parseURI)
|
||||||
|
|
||||||
import PgQuery(LoginAttempt(..), signInRole, setRole, resetRole)
|
import Auth (LoginAttempt(..), signInRole, setRole, resetRole)
|
||||||
import Codec.Binary.Base64.String (decode)
|
import Codec.Binary.Base64.String (decode)
|
||||||
|
|
||||||
|
authenticated :: forall s. Text -> Text ->
|
||||||
withDBConnection :: Pool Connection -> (Connection -> Application) -> Application
|
(Request -> H.Tx P.Postgres s Response) ->
|
||||||
withDBConnection pool app req respond =
|
Request -> H.Tx P.Postgres s Response
|
||||||
withResource pool (\c -> app c req respond)
|
authenticated currentRole anon app req = do
|
||||||
|
|
||||||
inTransaction :: (Connection -> Application) -> Connection -> Application
|
|
||||||
inTransaction app conn req respond =
|
|
||||||
finally (runRaw conn "begin" >> app conn req respond) (runRaw conn "commit")
|
|
||||||
|
|
||||||
withSavepoint :: (Connection -> Application) -> Connection -> Application
|
|
||||||
withSavepoint app conn req respond = do
|
|
||||||
runRaw conn "savepoint req_sp"
|
|
||||||
catch (app conn req respond) (\e -> let _ = (e::SomeException) in
|
|
||||||
runRaw conn "rollback to savepoint req_sp" >> throw e)
|
|
||||||
|
|
||||||
authenticated :: BS.ByteString -> (Connection -> Application) ->
|
|
||||||
Connection -> Application
|
|
||||||
authenticated anon app conn req respond = do
|
|
||||||
attempt <- httpRequesterRole (requestHeaders req)
|
attempt <- httpRequesterRole (requestHeaders req)
|
||||||
case attempt of
|
case attempt of
|
||||||
MalformedAuth ->
|
MalformedAuth ->
|
||||||
respond $ responseLBS status400 [] "Malformed basic auth header"
|
return $ responseLBS status400 [] "Malformed basic auth header"
|
||||||
LoginFailed ->
|
LoginFailed ->
|
||||||
respond $ responseLBS status401 [] "Invalid username or password"
|
return $ responseLBS status401 [] "Invalid username or password"
|
||||||
LoginSuccess role ->
|
LoginSuccess role -> if role /= currentRole then runInRole role else app req
|
||||||
bracket_ (setRole conn role) (resetRole conn) $ app conn req respond
|
NoCredentials -> if anon /= currentRole then runInRole anon else app req
|
||||||
NoCredentials ->
|
|
||||||
bracket_ (setRole conn anon) (resetRole conn) $ app conn req respond
|
|
||||||
|
|
||||||
where
|
where
|
||||||
httpRequesterRole :: RequestHeaders -> IO 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 BS.split ' ' (cs auth) of
|
case split (==' ') (cs auth) of
|
||||||
("Basic" : b64 : _) ->
|
("Basic" : b64 : _) ->
|
||||||
case BS.split ':' $ cs (decode $ cs b64) of
|
case split (==':') (cs . decode . cs $ b64) of
|
||||||
(u:p:_) -> signInRole u p conn
|
(u:p:_) -> signInRole u p
|
||||||
_ -> return MalformedAuth
|
_ -> return MalformedAuth
|
||||||
_ -> return NoCredentials
|
_ -> return NoCredentials
|
||||||
|
|
||||||
instance ToJSON SqlError where
|
runInRole :: Text -> H.Tx P.Postgres s Response
|
||||||
toJSON t = object [
|
runInRole r = do
|
||||||
"error" .= object [
|
setRole r
|
||||||
"code" .= seNativeError t
|
res <- app req
|
||||||
, "message" .= seErrorMsg t
|
resetRole
|
||||||
, "state" .= seState t
|
return res
|
||||||
]
|
|
||||||
]
|
|
||||||
|
|
||||||
clientErrors :: Application -> Application
|
|
||||||
clientErrors app req respond =
|
|
||||||
catchJust isPgException (app req respond) $ \err ->
|
|
||||||
respond $ if seState err == "42P01"
|
|
||||||
then responseLBS status404 [] ""
|
|
||||||
else responseLBS status400 [(hContentType, "application/json")] (encode err)
|
|
||||||
|
|
||||||
where
|
|
||||||
isPgException :: SqlError -> Maybe SqlError
|
|
||||||
isPgException = Just
|
|
||||||
|
|
||||||
|
|
||||||
redirectInsecure :: Application -> Application
|
redirectInsecure :: Application -> Application
|
||||||
|
|||||||
+193
-221
@@ -1,248 +1,220 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
|
||||||
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
-- {{{ Imports
|
module PgQuery where
|
||||||
|
|
||||||
module PgQuery (
|
import RangeQuery
|
||||||
getRows
|
|
||||||
, insert
|
|
||||||
, upsert
|
|
||||||
, addUser
|
|
||||||
, signInRole
|
|
||||||
, setRole
|
|
||||||
, resetRole
|
|
||||||
, checkPass
|
|
||||||
, RangedResult(..)
|
|
||||||
, LoginAttempt(..)
|
|
||||||
, DbRole
|
|
||||||
) where
|
|
||||||
|
|
||||||
import Data.Text (Text)
|
import qualified Hasql as H
|
||||||
import Data.String.Conversions (cs)
|
import qualified Hasql.Postgres as P
|
||||||
import Data.Functor ( (<$>) )
|
import qualified Hasql.Backend as B
|
||||||
import Data.Maybe (fromMaybe, mapMaybe)
|
|
||||||
import Data.List (intersperse, intercalate)
|
|
||||||
import Data.List.Split (splitOn)
|
|
||||||
import Data.Monoid ((<>), mconcat)
|
|
||||||
import qualified Data.Map as M
|
|
||||||
|
|
||||||
import Control.Monad (join, void)
|
|
||||||
|
|
||||||
import qualified RangeQuery as R
|
|
||||||
import qualified Data.ByteString.Char8 as BS
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
|
||||||
|
|
||||||
import Database.HDBC hiding (colType, colNullable)
|
|
||||||
import Database.HDBC.PostgreSQL
|
|
||||||
|
|
||||||
|
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 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)
|
||||||
|
import Data.String.Conversions (cs)
|
||||||
|
import qualified Data.Aeson as JSON
|
||||||
|
import qualified Data.List as L
|
||||||
|
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
|
||||||
|
|
||||||
import Types (SqlRow(..), getRow, sqlRowColumns, sqlRowValues)
|
type PStmt = H.Stmt P.Postgres
|
||||||
import Crypto.BCrypt (hashPasswordUsingPolicy, fastBcryptHashingPolicy, validatePassword)
|
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 :: T.Text
|
||||||
data RangedResult = RangedResult {
|
, qtName :: T.Text
|
||||||
rrFrom :: Int
|
|
||||||
, rrTo :: Int
|
|
||||||
, rrTotal :: Int
|
|
||||||
, rrBody :: BL.ByteString
|
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
type QuotedSql = (String, [SqlValue])
|
|
||||||
type Schema = String
|
|
||||||
type DbRole = BS.ByteString
|
|
||||||
|
|
||||||
data LoginAttempt =
|
|
||||||
NoCredentials
|
|
||||||
| MalformedAuth
|
|
||||||
| LoginFailed
|
|
||||||
| LoginSuccess DbRole
|
|
||||||
deriving (Eq, Show)
|
|
||||||
|
|
||||||
getRows :: Schema -> String -> Net.Query -> Maybe R.NonnegRange -> Connection -> IO RangedResult
|
|
||||||
getRows schema table qq range conn = do
|
|
||||||
query <- populateSql conn
|
|
||||||
$ globalAndLimitedCounts schema table qq <>
|
|
||||||
jsonArrayRows
|
|
||||||
(selectStarClause schema table
|
|
||||||
<> whereClause qq
|
|
||||||
<> orderClause qq
|
|
||||||
<> limitClause range)
|
|
||||||
r <- quickQuery conn query []
|
|
||||||
|
|
||||||
return $ case r of
|
|
||||||
[[total, _, SqlNull]] -> RangedResult offset 0 (fromSql total) "[]"
|
|
||||||
[[total, limited_total, json]] ->
|
|
||||||
RangedResult offset (offset + fromSql limited_total - 1)
|
|
||||||
(fromSql total) (fromSql json)
|
|
||||||
_ -> RangedResult 0 0 0 "[]"
|
|
||||||
|
|
||||||
where
|
|
||||||
offset = fromMaybe 0 $ R.offset <$> range
|
|
||||||
|
|
||||||
|
|
||||||
whereClause :: Net.Query -> QuotedSql
|
|
||||||
whereClause qs =
|
|
||||||
if null qs then ("", []) else (" where ", []) <> conjunction
|
|
||||||
|
|
||||||
where
|
|
||||||
cols = [ col | col <- qs, fst col `notElem` ["order"] ]
|
|
||||||
conjunction = mconcat $ intersperse (" and ", []) (map wherePred cols)
|
|
||||||
|
|
||||||
|
|
||||||
orderClause :: Net.Query -> QuotedSql
|
|
||||||
orderClause qs = do
|
|
||||||
let order = fromMaybe "" $ join $ lookup "order" qs
|
|
||||||
terms = mapMaybe parseOrderTerm $ splitOn "," $ cs order
|
|
||||||
termPred = mconcat $ intersperse (", ", []) (map orderTermSql terms)
|
|
||||||
|
|
||||||
if null terms
|
|
||||||
then ("", [])
|
|
||||||
else (" order by ", []) <> termPred
|
|
||||||
|
|
||||||
where
|
|
||||||
parseOrderTerm :: String -> Maybe OrderTerm
|
|
||||||
parseOrderTerm s =
|
|
||||||
case splitOn "." s of
|
|
||||||
[d,c] ->
|
|
||||||
if d `elem` ["asc", "desc"]
|
|
||||||
then Just $ OrderTerm d c
|
|
||||||
else Nothing
|
|
||||||
_ -> Nothing
|
|
||||||
|
|
||||||
orderTermSql :: OrderTerm -> QuotedSql
|
|
||||||
orderTermSql t =
|
|
||||||
("%I " <> otDirection t, [toSql $ otColumn t])
|
|
||||||
|
|
||||||
|
|
||||||
data OrderTerm = OrderTerm {
|
data OrderTerm = OrderTerm {
|
||||||
otDirection :: String
|
otTerm :: T.Text
|
||||||
, otColumn :: String
|
, otDirection :: BS.ByteString
|
||||||
}
|
}
|
||||||
|
|
||||||
|
limitT :: Maybe NonnegRange -> StatementT
|
||||||
|
limitT r q =
|
||||||
|
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
|
||||||
|
|
||||||
wherePred :: Net.QueryItem -> QuotedSql
|
whereT :: Net.Query -> StatementT
|
||||||
wherePred (column, predicate) =
|
whereT params q =
|
||||||
("%I " <> op <> "%L", map toSql [column, value])
|
if L.null cols
|
||||||
|
then q
|
||||||
|
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)
|
||||||
|
|
||||||
|
orderT :: [OrderTerm] -> StatementT
|
||||||
|
orderT ts q =
|
||||||
|
if L.null ts
|
||||||
|
then q
|
||||||
|
else q <> B.Stmt " order by " empty True <> clause
|
||||||
|
where
|
||||||
|
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
|
||||||
|
queryTerm :: OrderTerm -> PStmt
|
||||||
|
queryTerm t = B.Stmt
|
||||||
|
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
|
||||||
|
<> cs (otDirection t) <> " ")
|
||||||
|
empty True
|
||||||
|
|
||||||
|
parentheticT :: StatementT
|
||||||
|
parentheticT s =
|
||||||
|
s { B.stmtTemplate = " (" <> B.stmtTemplate s <> ") " }
|
||||||
|
|
||||||
|
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)
|
||||||
|
|
||||||
|
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 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 s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
|
||||||
|
|
||||||
|
selectStar :: QualifiedTable -> PStmt
|
||||||
|
selectStar t = B.Stmt ("select * from " <> fromQt t) empty True
|
||||||
|
|
||||||
|
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 <> " (" <>
|
||||||
|
T.intercalate ", " (map pgFmtIdent cols) <>
|
||||||
|
") values ("
|
||||||
|
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
|
||||||
|
<> ") returning row_to_json(" <> fromQt t <> ".*)")
|
||||||
|
empty True
|
||||||
|
|
||||||
|
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 -> [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 -> PStmt
|
||||||
|
wherePred (col, predicate) = B.Stmt
|
||||||
|
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs sqlValue)
|
||||||
|
empty True
|
||||||
|
|
||||||
where
|
where
|
||||||
opCode:rest = BS.split '.' $ fromMaybe "." predicate
|
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
|
||||||
value = BS.intercalate "." rest
|
value = T.intercalate "." rest
|
||||||
|
|
||||||
|
star c = if c == '*' then '%' else c
|
||||||
|
unknownLiteral = (<> "::unknown ") . pgFmtLit
|
||||||
|
|
||||||
|
sqlValue = case opCode of
|
||||||
|
"like" -> unknownLiteral $ T.map star value
|
||||||
|
"ilike" -> unknownLiteral $ T.map star value
|
||||||
|
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
|
||||||
|
_ -> unknownLiteral value
|
||||||
|
|
||||||
op = case opCode of
|
op = case opCode of
|
||||||
"eq" -> "="
|
"eq" -> "="
|
||||||
"gt" -> ">"
|
"gt" -> ">"
|
||||||
"lt" -> "<"
|
"lt" -> "<"
|
||||||
"gte" -> ">="
|
"gte" -> ">="
|
||||||
"lte" -> "<="
|
"lte" -> "<="
|
||||||
"neq" -> "<>"
|
"neq" -> "<>"
|
||||||
_ -> "="
|
"like"-> "like"
|
||||||
|
"ilike"-> "ilike"
|
||||||
limitClause :: Maybe R.NonnegRange -> QuotedSql
|
"in" -> "in"
|
||||||
limitClause range =
|
_ -> "="
|
||||||
(" LIMIT %s OFFSET %s ", [toSql limit, toSql offset])
|
|
||||||
|
|
||||||
|
orderParse :: Net.Query -> [OrderTerm]
|
||||||
|
orderParse q =
|
||||||
|
mapMaybe orderParseTerm . T.split (==',') $ cs order
|
||||||
where
|
where
|
||||||
limit = fromMaybe "ALL" $ show <$> (R.limit =<< range)
|
order = fromMaybe "" $ join (lookup "order" q)
|
||||||
offset = fromMaybe 0 $ R.offset <$> range
|
|
||||||
|
|
||||||
globalAndLimitedCounts :: Schema -> String -> Net.Query -> QuotedSql
|
orderParseTerm :: T.Text -> Maybe OrderTerm
|
||||||
globalAndLimitedCounts schema table qq =
|
orderParseTerm s =
|
||||||
(" select ", [])
|
case T.split (=='.') s of
|
||||||
<> ("(select count(1) from %I.%I ", map toSql [schema, table])
|
[c,d] ->
|
||||||
<> whereClause qq
|
if d `elem` ["asc", "desc"]
|
||||||
<> ("), count(t), ", [])
|
then Just $ OrderTerm c $
|
||||||
|
if d == "asc" then "asc" else "desc"
|
||||||
|
else Nothing
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
selectStarClause :: Schema -> String -> QuotedSql
|
commaq :: PStmt
|
||||||
selectStarClause schema table =
|
commaq = B.Stmt ", " empty True
|
||||||
(" select * from %I.%I ", map toSql [schema, table])
|
|
||||||
|
|
||||||
jsonArrayRows :: QuotedSql -> QuotedSql
|
andq :: PStmt
|
||||||
jsonArrayRows q =
|
andq = B.Stmt " and " empty True
|
||||||
("array_to_json(array_agg(row_to_json(t))) from (", []) <> q <> (") t", [])
|
|
||||||
|
|
||||||
insert :: Schema -> Text -> SqlRow -> Connection -> IO (M.Map String SqlValue)
|
pgFmtIdent :: T.Text -> T.Text
|
||||||
insert schema table row conn = do
|
pgFmtIdent x =
|
||||||
sql <- populateSql conn $ insertClause schema table row
|
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
|
||||||
stmt <- prepare conn sql
|
if escaped =~ danger
|
||||||
_ <- execute stmt $ sqlRowValues row
|
then "\"" <> escaped <> "\""
|
||||||
Just m <- fetchRowMap stmt
|
else escaped
|
||||||
return m
|
|
||||||
|
|
||||||
addUser :: BS.ByteString -> BS.ByteString -> BS.ByteString -> Connection -> IO ()
|
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: T.Text
|
||||||
addUser identity pass role conn = do
|
|
||||||
hashed <- hashPasswordUsingPolicy fastBcryptHashingPolicy $ cs pass
|
|
||||||
_ <- insert "dbapi" "auth" (SqlRow [
|
|
||||||
("id", toSql identity), ("pass", toSql hashed), ("rolname", toSql role)
|
|
||||||
]) conn
|
|
||||||
return ()
|
|
||||||
|
|
||||||
signInRole :: BS.ByteString -> BS.ByteString -> Connection -> IO LoginAttempt
|
pgFmtLit :: T.Text -> T.Text
|
||||||
signInRole user pass conn = do
|
pgFmtLit x =
|
||||||
u <- quickQuery conn "select pass, rolname from dbapi.auth where id = ?" [toSql user]
|
let trimmed = trimNullChars x
|
||||||
return $ case u of
|
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
|
||||||
[[hashed, role]] ->
|
slashed = T.replace "\\" "\\\\" escaped in
|
||||||
if checkPass (fromSql hashed) (cs pass)
|
cs $ if escaped =~ ("\\\\" :: T.Text)
|
||||||
then LoginSuccess $ fromSql role
|
then "E" <> slashed
|
||||||
else LoginFailed
|
else slashed
|
||||||
_ -> LoginFailed
|
|
||||||
|
|
||||||
checkPass :: BS.ByteString -> BS.ByteString -> Bool
|
trimNullChars :: T.Text -> T.Text
|
||||||
checkPass = validatePassword
|
trimNullChars = T.takeWhile (/= '\x0')
|
||||||
|
|
||||||
upsert :: Schema -> Text -> SqlRow -> Net.Query -> Connection -> IO (M.Map String SqlValue)
|
fromQt :: QualifiedTable -> T.Text
|
||||||
upsert schema table row qq conn = do
|
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
|
||||||
sql <- populateSql conn $ upsertClause schema table row qq
|
|
||||||
stmt <- prepare conn sql
|
|
||||||
_ <- execute stmt $ join $ replicate 2 $ sqlRowValues row
|
|
||||||
Just m <- fetchRowMap stmt
|
|
||||||
return m
|
|
||||||
|
|
||||||
placeholders :: String -> SqlRow -> String
|
unquoted :: JSON.Value -> T.Text
|
||||||
placeholders symbol = intercalate ", " . map (const symbol) . getRow
|
unquoted (JSON.String t) = t
|
||||||
|
unquoted (JSON.Number n) =
|
||||||
insertClause :: Schema -> Text -> SqlRow -> QuotedSql
|
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
|
||||||
insertClause schema table (SqlRow []) =
|
unquoted (JSON.Bool b) = cs . show $ b
|
||||||
("insert into %I.%I default values returning *", [toSql schema, toSql table])
|
unquoted _ = ""
|
||||||
insertClause schema table row =
|
|
||||||
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
|
|
||||||
map toSql $ cs schema : table : sqlRowColumns row)
|
|
||||||
<> (" values (" ++ placeholders "?" row ++ ") returning *", sqlRowValues row)
|
|
||||||
|
|
||||||
|
|
||||||
insertClauseViaSelect :: Schema -> Text -> SqlRow -> QuotedSql
|
|
||||||
insertClauseViaSelect schema table row =
|
|
||||||
("insert into %I.%I (" ++ placeholders "%I" row ++ ")",
|
|
||||||
map toSql $ cs schema : table : sqlRowColumns row)
|
|
||||||
<> (" select " ++ placeholders "?" row, sqlRowValues row)
|
|
||||||
|
|
||||||
updateClause :: Schema -> Text -> SqlRow -> QuotedSql
|
|
||||||
updateClause schema table row =
|
|
||||||
("update %I.%I set (" ++ placeholders "%I" row ++ ")",
|
|
||||||
map toSql $ cs schema : table : sqlRowColumns row)
|
|
||||||
<> (" = (" ++ placeholders "?" row ++ ")", [])
|
|
||||||
|
|
||||||
upsertClause :: Schema -> Text -> SqlRow -> Net.Query -> QuotedSql
|
|
||||||
upsertClause schema table row qq =
|
|
||||||
("with upsert as (", []) <> updateClause schema table row
|
|
||||||
<> whereClause qq
|
|
||||||
<> (" returning *) ", []) <> insertClauseViaSelect schema table row
|
|
||||||
<> (" where not exists (select * from upsert) returning *", [])
|
|
||||||
|
|
||||||
populateSql :: Connection -> QuotedSql -> IO String
|
|
||||||
populateSql conn sql = do
|
|
||||||
[[escaped]] <- quickQuery conn q (snd sql)
|
|
||||||
return $ fromSql escaped
|
|
||||||
|
|
||||||
where
|
|
||||||
q = concat [ "select format('", fst sql, "', ", ph (snd sql), ")" ]
|
|
||||||
|
|
||||||
ph :: [a] -> String
|
|
||||||
ph = intercalate ", " . map (const "?::varchar")
|
|
||||||
|
|
||||||
setRole :: Connection -> DbRole -> IO ()
|
|
||||||
setRole conn role = do
|
|
||||||
query <- populateSql conn ("set role %I", [toSql role])
|
|
||||||
void $ run conn query []
|
|
||||||
|
|
||||||
resetRole :: Connection -> IO ()
|
|
||||||
resetRole conn = void $ run conn "reset role" []
|
|
||||||
|
|||||||
+136
-157
@@ -1,80 +1,155 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
{-# LANGUAGE QuasiQuotes, OverloadedStrings, TypeSynonymInstances,
|
||||||
|
MultiParamTypeClasses, ScopedTypeVariables #-}
|
||||||
module PgStructure where
|
module PgStructure where
|
||||||
|
|
||||||
import Data.Functor ( (<$>) )
|
import PgQuery (QualifiedTable(..))
|
||||||
import Data.Maybe (mapMaybe)
|
import Data.Text hiding (foldl, map, zipWith, concat)
|
||||||
|
import Data.Aeson
|
||||||
|
import Data.Functor.Identity
|
||||||
|
import Data.String.Conversions (cs)
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
|
import Control.Applicative ( (<$>) )
|
||||||
|
|
||||||
import Control.Applicative ( (<*>) )
|
|
||||||
|
|
||||||
import qualified Data.ByteString.Lazy as BL
|
|
||||||
import Data.List.Split (splitOn)
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
|
||||||
import qualified Data.Map as Map
|
import qualified Data.Map as Map
|
||||||
|
|
||||||
import Database.HDBC hiding (colType, colNullable)
|
import qualified Hasql as H
|
||||||
import Database.HDBC.PostgreSQL
|
import qualified Hasql.Postgres as P
|
||||||
|
|
||||||
import Data.Aeson ((.=))
|
foreignKeys :: QualifiedTable -> H.Tx P.Postgres s (Map.Map Text ForeignKey)
|
||||||
|
foreignKeys table = do
|
||||||
|
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
|
||||||
|
join information_schema.key_column_usage AS kcu
|
||||||
|
on tc.constraint_name = kcu.constraint_name
|
||||||
|
join information_schema.constraint_column_usage AS ccu
|
||||||
|
on ccu.constraint_name = tc.constraint_name
|
||||||
|
where constraint_type = 'FOREIGN KEY'
|
||||||
|
and tc.table_name=? and tc.table_schema = ?
|
||||||
|
order by kcu.column_name
|
||||||
|
|] (qtName table) (qtSchema table)
|
||||||
|
|
||||||
|
return $ foldl addKey Map.empty r
|
||||||
|
where
|
||||||
|
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 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 P.Postgres s [Column]
|
||||||
|
columns table = do
|
||||||
|
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 (addFK fks . columnFromRow) cols
|
||||||
|
|
||||||
|
where
|
||||||
|
addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks }
|
||||||
|
|
||||||
|
|
||||||
|
primaryKeyColumns :: QualifiedTable -> H.Tx P.Postgres s [Text]
|
||||||
|
primaryKeyColumns table = do
|
||||||
|
r <- H.listEx $ [H.stmt|
|
||||||
|
select kc.column_name
|
||||||
|
from
|
||||||
|
information_schema.table_constraints tc,
|
||||||
|
information_schema.key_column_usage kc
|
||||||
|
where
|
||||||
|
tc.constraint_type = 'PRIMARY KEY'
|
||||||
|
and kc.table_name = tc.table_name and kc.table_schema = tc.table_schema
|
||||||
|
and kc.constraint_name = tc.constraint_name
|
||||||
|
and kc.table_schema = ?
|
||||||
|
and kc.table_name = ? |] (qtSchema table) (qtName table)
|
||||||
|
return $ map runIdentity r
|
||||||
|
|
||||||
|
|
||||||
|
toBool :: Text -> Bool
|
||||||
|
toBool = (== "YES")
|
||||||
|
|
||||||
data Table = Table {
|
data Table = Table {
|
||||||
tableSchema :: String
|
tableSchema :: Text
|
||||||
, tableName :: String
|
, tableName :: Text
|
||||||
, tableInsertable :: Bool
|
, tableInsertable :: Bool
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
instance JSON.ToJSON Table where
|
|
||||||
toJSON v = JSON.object [
|
|
||||||
"schema" .= tableSchema v
|
|
||||||
, "name" .= tableName v
|
|
||||||
, "insertable" .= tableInsertable v ]
|
|
||||||
|
|
||||||
toBool :: String -> Bool
|
|
||||||
toBool = (== "YES")
|
|
||||||
|
|
||||||
data ForeignKey = ForeignKey {
|
data ForeignKey = ForeignKey {
|
||||||
fkTable::String, fkCol::String
|
fkTable::Text, fkCol::Text
|
||||||
} deriving (Eq, Show)
|
} deriving (Eq, Show)
|
||||||
|
|
||||||
instance JSON.ToJSON ForeignKey where
|
|
||||||
toJSON fk = JSON.object ["table".=fkTable fk, "column".=fkCol fk]
|
|
||||||
|
|
||||||
foreignKeys :: String -> String -> Connection -> IO (Map.Map String ForeignKey)
|
|
||||||
foreignKeys schema table conn = do
|
|
||||||
r <- quickQuery conn
|
|
||||||
"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 \
|
|
||||||
\ join information_schema.key_column_usage AS kcu \
|
|
||||||
\ on tc.constraint_name = kcu.constraint_name \
|
|
||||||
\ join information_schema.constraint_column_usage AS ccu \
|
|
||||||
\ on ccu.constraint_name = tc.constraint_name \
|
|
||||||
\where constraint_type = 'FOREIGN KEY' \
|
|
||||||
\ and tc.table_name=? and tc.table_schema = ? \
|
|
||||||
\order by kcu.column_name" (map toSql [table, schema])
|
|
||||||
return $ foldl addKey Map.empty $ map (map fromSql) r
|
|
||||||
where
|
|
||||||
addKey m [col, ftab, fcol] = Map.insert col (ForeignKey ftab fcol) m
|
|
||||||
addKey m _ = m --should never happen
|
|
||||||
|
|
||||||
data Column = Column {
|
data Column = Column {
|
||||||
colSchema :: String
|
colSchema :: Text
|
||||||
, colTable :: String
|
, colTable :: Text
|
||||||
, colName :: String
|
, colName :: Text
|
||||||
, colPosition :: Int
|
, colPosition :: Int
|
||||||
, colNullable :: Bool
|
, colNullable :: Bool
|
||||||
, colType :: String
|
, colType :: Text
|
||||||
, colUpdatable :: Bool
|
, colUpdatable :: Bool
|
||||||
, colMaxLen :: Maybe Int
|
, colMaxLen :: Maybe Int
|
||||||
, colPrecision :: Maybe Int
|
, colPrecision :: Maybe Int
|
||||||
, colDefault :: Maybe String
|
, colDefault :: Maybe Text
|
||||||
, colEnum :: Maybe [String]
|
, colEnum :: [Text]
|
||||||
, colFK :: Maybe ForeignKey
|
, colFK :: Maybe ForeignKey
|
||||||
} deriving (Show)
|
} deriving (Show)
|
||||||
|
|
||||||
instance JSON.ToJSON Column where
|
tableFromRow :: (Text, Text, Text) -> Table
|
||||||
toJSON c = JSON.object [
|
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 ToJSON Column where
|
||||||
|
toJSON c = object [
|
||||||
"schema" .= colSchema c
|
"schema" .= colSchema c
|
||||||
, "name" .= colName c
|
, "name" .= colName c
|
||||||
, "position" .= colPosition c
|
, "position" .= colPosition c
|
||||||
@@ -87,107 +162,11 @@ instance JSON.ToJSON Column where
|
|||||||
, "default" .= colDefault c
|
, "default" .= colDefault c
|
||||||
, "enum" .= colEnum c ]
|
, "enum" .= colEnum c ]
|
||||||
|
|
||||||
data TableOptions = TableOptions {
|
instance ToJSON ForeignKey where
|
||||||
tblOptcolumns :: [Column]
|
toJSON fk = object ["table".=fkTable fk, "column".=fkCol fk]
|
||||||
, tblOptpkey :: [String]
|
|
||||||
}
|
|
||||||
|
|
||||||
instance JSON.ToJSON TableOptions where
|
instance ToJSON Table where
|
||||||
toJSON t = JSON.object [
|
toJSON v = object [
|
||||||
"columns" .= tblOptcolumns t
|
"schema" .= tableSchema v
|
||||||
, "pkey" .= tblOptpkey t ]
|
, "name" .= tableName v
|
||||||
|
, "insertable" .= tableInsertable v ]
|
||||||
tables :: String -> Connection -> IO [Table]
|
|
||||||
tables s conn = do
|
|
||||||
r <- quickQuery conn
|
|
||||||
"select table_schema, table_name,\
|
|
||||||
\ is_insertable_into\
|
|
||||||
\ from information_schema.tables\
|
|
||||||
\ where table_schema = ?\
|
|
||||||
\ order by table_name" [toSql s]
|
|
||||||
return $ mapMaybe mkTable r
|
|
||||||
|
|
||||||
where
|
|
||||||
mkTable [schema, name, insertable] =
|
|
||||||
Just $ Table (fromSql schema)
|
|
||||||
(fromSql name)
|
|
||||||
(toBool (fromSql insertable))
|
|
||||||
mkTable _ = Nothing
|
|
||||||
|
|
||||||
columns :: String -> String -> Connection -> IO [Column]
|
|
||||||
columns s t conn = do
|
|
||||||
r <- quickQuery conn
|
|
||||||
"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" [toSql s, toSql t]
|
|
||||||
fks <- foreignKeys s t conn
|
|
||||||
let lookupFK (_:_:name:_) = Map.lookup (fromSql name) fks
|
|
||||||
lookupFK _ = Nothing
|
|
||||||
let cols = zipWith ($) (map mkColumn r) (map lookupFK r)
|
|
||||||
return cols
|
|
||||||
|
|
||||||
where
|
|
||||||
mkColumn [schema, table, name, pos, nullable, colT, updatable, maxlen, precision, defVal, enum] = Column (fromSql schema)
|
|
||||||
(fromSql table)
|
|
||||||
(fromSql name)
|
|
||||||
(fromSql pos)
|
|
||||||
(toBool (fromSql nullable))
|
|
||||||
(fromSql colT)
|
|
||||||
(toBool (fromSql updatable))
|
|
||||||
(fromSql maxlen)
|
|
||||||
(fromSql precision)
|
|
||||||
(fromSql defVal)
|
|
||||||
(splitOn "," <$> fromSql enum)
|
|
||||||
mkColumn _ = error $ "Incomplete column data received for table " ++
|
|
||||||
t ++ " in schema " ++ s ++ "."
|
|
||||||
|
|
||||||
printTables :: String -> Connection -> IO BL.ByteString
|
|
||||||
printTables schema conn = JSON.encode <$> tables schema conn
|
|
||||||
|
|
||||||
printColumns :: String -> String -> Connection -> IO BL.ByteString
|
|
||||||
printColumns schema table conn =
|
|
||||||
JSON.encode <$> (TableOptions <$> cols <*> pkey)
|
|
||||||
where
|
|
||||||
cols :: IO [Column]
|
|
||||||
cols = columns schema table conn
|
|
||||||
pkey :: IO [String]
|
|
||||||
pkey = primaryKeyColumns schema table conn
|
|
||||||
|
|
||||||
primaryKeyColumns :: String -> String -> Connection -> IO [String]
|
|
||||||
primaryKeyColumns s t conn = do
|
|
||||||
r <- quickQuery conn
|
|
||||||
"select kc.column_name \
|
|
||||||
\ from \
|
|
||||||
\ information_schema.table_constraints tc, \
|
|
||||||
\ information_schema.key_column_usage kc \
|
|
||||||
\where \
|
|
||||||
\ tc.constraint_type = 'PRIMARY KEY' \
|
|
||||||
\ and kc.table_name = tc.table_name and kc.table_schema = tc.table_schema \
|
|
||||||
\ and kc.constraint_name = tc.constraint_name \
|
|
||||||
\ and kc.table_schema = ? \
|
|
||||||
\ and kc.table_name = ?" [toSql s, toSql t]
|
|
||||||
return $ map fromSql (concat r)
|
|
||||||
|
|||||||
+36
-33
@@ -1,10 +1,16 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
module RangeQuery (
|
||||||
|
rangeParse
|
||||||
module RangeQuery where
|
, rangeRequested
|
||||||
|
, rangeLimit
|
||||||
|
, rangeOffset
|
||||||
|
, NonnegRange
|
||||||
|
) where
|
||||||
|
|
||||||
import Control.Applicative
|
import Control.Applicative
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
|
|
||||||
|
import qualified Data.ByteString.Char8 as BS
|
||||||
|
|
||||||
import Data.Ranged.Boundaries
|
import Data.Ranged.Boundaries
|
||||||
import Data.Ranged.Ranges
|
import Data.Ranged.Ranges
|
||||||
|
|
||||||
@@ -16,6 +22,33 @@ import Data.Maybe (fromMaybe, listToMaybe)
|
|||||||
|
|
||||||
type NonnegRange = Range Int
|
type NonnegRange = Range Int
|
||||||
|
|
||||||
|
rangeParse :: BS.ByteString -> Maybe NonnegRange
|
||||||
|
rangeParse range = do
|
||||||
|
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
|
||||||
|
|
||||||
|
parsedRange <- listToMaybe (range =~ rangeRegex :: [[BS.ByteString]])
|
||||||
|
|
||||||
|
let [_, from, to] = readMaybe . cs <$> parsedRange
|
||||||
|
let lower = fromMaybe emptyRange (rangeGeq <$> from)
|
||||||
|
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
|
||||||
|
|
||||||
|
return $ rangeIntersection lower upper
|
||||||
|
|
||||||
|
rangeRequested :: RequestHeaders -> Maybe NonnegRange
|
||||||
|
rangeRequested = (rangeParse =<<) . lookup hRange
|
||||||
|
|
||||||
|
rangeLimit :: NonnegRange -> Maybe Int
|
||||||
|
rangeLimit range =
|
||||||
|
case [rangeLower range, rangeUpper range]
|
||||||
|
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
|
||||||
|
_ -> Nothing
|
||||||
|
|
||||||
|
rangeOffset :: NonnegRange -> Int
|
||||||
|
rangeOffset range =
|
||||||
|
case rangeLower range
|
||||||
|
of BoundaryBelow from -> from
|
||||||
|
_ -> error "range without lower bound" -- should never happen
|
||||||
|
|
||||||
rangeGeq :: Int -> NonnegRange
|
rangeGeq :: Int -> NonnegRange
|
||||||
rangeGeq n =
|
rangeGeq n =
|
||||||
Range (BoundaryBelow n) BoundaryAboveAll
|
Range (BoundaryBelow n) BoundaryAboveAll
|
||||||
@@ -23,33 +56,3 @@ rangeGeq n =
|
|||||||
rangeLeq :: Int -> NonnegRange
|
rangeLeq :: Int -> NonnegRange
|
||||||
rangeLeq n =
|
rangeLeq n =
|
||||||
Range BoundaryBelowAll (BoundaryAbove n)
|
Range BoundaryBelowAll (BoundaryAbove n)
|
||||||
|
|
||||||
parseRange :: String -> Maybe NonnegRange
|
|
||||||
parseRange range = do
|
|
||||||
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: String
|
|
||||||
|
|
||||||
parsedRange <- listToMaybe (range =~ rangeRegex :: [[String]])
|
|
||||||
|
|
||||||
let [_, from, to] = readMaybe <$> parsedRange
|
|
||||||
let lower = fromMaybe emptyRange (rangeGeq <$> from)
|
|
||||||
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
|
|
||||||
|
|
||||||
return $ rangeIntersection lower upper
|
|
||||||
|
|
||||||
requestedRange :: RequestHeaders -> Maybe NonnegRange
|
|
||||||
requestedRange hdrs = parseRange =<< cs <$> lookup hRange hdrs
|
|
||||||
|
|
||||||
requestedContentRange :: RequestHeaders -> Maybe NonnegRange
|
|
||||||
requestedContentRange hdrs = parseRange =<< cs <$> lookup "Content-Range" hdrs
|
|
||||||
|
|
||||||
limit :: NonnegRange -> Maybe Int
|
|
||||||
limit range =
|
|
||||||
case [rangeLower range, rangeUpper range]
|
|
||||||
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
|
|
||||||
_ -> Nothing
|
|
||||||
|
|
||||||
offset :: NonnegRange -> Int
|
|
||||||
offset range =
|
|
||||||
case rangeLower range
|
|
||||||
of BoundaryBelow from -> from
|
|
||||||
_ -> error "range without lower bound" -- should never happen
|
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
{-# OPTIONS_GHC -fno-warn-orphans #-}
|
||||||
|
|
||||||
module Types where
|
module Types where
|
||||||
|
|
||||||
import Database.HDBC (toSql, iToSql, SqlValue(..))
|
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import Data.Aeson.Types (Parser)
|
import Data.Aeson.Types (Parser)
|
||||||
|
|
||||||
@@ -12,7 +9,6 @@ import Data.HashMap.Strict (foldlWithKey')
|
|||||||
import Data.Text (Text)
|
import Data.Text (Text)
|
||||||
import Data.Text.Encoding (decodeUtf8)
|
import Data.Text.Encoding (decodeUtf8)
|
||||||
import Data.Time.Calendar (showGregorian)
|
import Data.Time.Calendar (showGregorian)
|
||||||
|
|
||||||
import Control.Monad (mzero)
|
import Control.Monad (mzero)
|
||||||
|
|
||||||
instance JSON.FromJSON SqlValue where
|
instance JSON.FromJSON SqlValue where
|
||||||
|
|||||||
Binary file not shown.
|
After Width: | Height: | Size: 2.9 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 36 KiB |
+19
-13
@@ -1,24 +1,30 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module Feature.AuthSpec where
|
module Feature.AuthSpec where
|
||||||
|
|
||||||
-- {{{ Imports
|
-- {{{ Imports
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = around appWithFixture $
|
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` 400 -- TODO: should be 404
|
$ describe "authorization" $ do
|
||||||
it "indicates login failure" $ do
|
|
||||||
let auth = authHeader "dbapi_test_author_a" "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
|
||||||
-- let auth = authHeader "dbapi_test_author_a" ""
|
let auth = authHeader "postgrest_test_author" "fakefake"
|
||||||
-- request methodGet "/authors_only" [auth] ""
|
request methodGet "/authors_only" [auth] ""
|
||||||
-- `shouldRespondWith` 200
|
`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
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Feature.CorsSpec where
|
module Feature.CorsSpec where
|
||||||
|
|
||||||
-- {{{ Imports
|
-- {{{ Imports
|
||||||
@@ -14,8 +12,7 @@ import Network.HTTP.Types
|
|||||||
-- }}}
|
-- }}}
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = around appWithFixture $
|
spec = around withApp $ describe "CORS" $ do
|
||||||
describe "CORS" $ do
|
|
||||||
let preflightHeaders = [
|
let preflightHeaders = [
|
||||||
("Accept", "*/*"),
|
("Accept", "*/*"),
|
||||||
("Origin", "http://example.com"),
|
("Origin", "http://example.com"),
|
||||||
@@ -56,6 +53,14 @@ spec = around appWithFixture $
|
|||||||
r <- request methodOptions "/" preflightHeaders ""
|
r <- request methodOptions "/" preflightHeaders ""
|
||||||
liftIO $ simpleBody r `shouldBe` ""
|
liftIO $ simpleBody r `shouldBe` ""
|
||||||
|
|
||||||
|
describe "regular request" $
|
||||||
|
it "exposes necesssary response headers" $ do
|
||||||
|
r <- request methodGet "/items" [("Origin", "http://example.com")] ""
|
||||||
|
liftIO $ simpleHeaders r `shouldSatisfy` matchHeader
|
||||||
|
"Access-Control-Expose-Headers"
|
||||||
|
"Content-Encoding, Content-Location, Content-Range, Content-Type, \
|
||||||
|
\Date, Location, Server, Transfer-Encoding, Range-Unit"
|
||||||
|
|
||||||
describe "postflight request" $
|
describe "postflight request" $
|
||||||
it "allows INFO body through even with CORS request headers present" $ do
|
it "allows INFO body through even with CORS request headers present" $ do
|
||||||
r <- request methodOptions "/items" normalCors ""
|
r <- request methodOptions "/items" normalCors ""
|
||||||
|
|||||||
@@ -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
|
||||||
+90
-29
@@ -1,8 +1,5 @@
|
|||||||
|
|
||||||
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
|
|
||||||
module Feature.InsertSpec where
|
module Feature.InsertSpec where
|
||||||
|
|
||||||
-- {{{ Imports
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
@@ -14,25 +11,26 @@ import qualified Data.Aeson as JSON
|
|||||||
import Data.Maybe (fromJust)
|
import Data.Maybe (fromJust)
|
||||||
import Network.HTTP.Types.Header
|
import Network.HTTP.Types.Header
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
|
import Control.Monad (replicateM_)
|
||||||
|
|
||||||
import TestTypes(IncPK, incStr, incNullableStr)
|
import TestTypes(IncPK(..), CompoundPK(..))
|
||||||
|
|
||||||
-- }}}
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = around appWithFixture $ do
|
spec = afterAll_ resetDb $ around withApp $ do
|
||||||
describe "Posting new record" $ do
|
describe "Posting new record" $ do
|
||||||
it "accepts disparate json types" $
|
after_ (clearTable "menagerie") . it "accepts disparate json types" $ do
|
||||||
post "/menagerie"
|
p <- post "/menagerie"
|
||||||
[json| {
|
[json| {
|
||||||
"integer": 13, "double": 3.14159, "varchar": "testing!"
|
"integer": 13, "double": 3.14159, "varchar": "testing!"
|
||||||
, "boolean": false, "date": "01/01/1900", "money": "$3.99"
|
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
|
||||||
, "enum": "foo"
|
, "enum": "foo"
|
||||||
} |]
|
} |]
|
||||||
`shouldRespondWith` 201
|
liftIO $ do
|
||||||
|
simpleBody p `shouldBe` ""
|
||||||
|
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
|
||||||
@@ -51,7 +49,7 @@ spec = around appWithFixture $ 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") $ do
|
||||||
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
|
||||||
@@ -59,7 +57,16 @@ spec = around appWithFixture $ 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" $
|
it "returns full details of inserted record if asked" $ do
|
||||||
|
p <- request methodPost "/no_pk"
|
||||||
|
[("Prefer", "return=representation")]
|
||||||
|
[json| { "a":"bar", "b":"baz" } |]
|
||||||
|
liftIO $ do
|
||||||
|
simpleBody p `shouldBe` [json| { "a":"bar", "b":"baz" } |]
|
||||||
|
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.bar&b=eq.baz"
|
||||||
|
simpleStatus p `shouldBe` created201
|
||||||
|
|
||||||
|
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 {
|
||||||
@@ -72,7 +79,7 @@ spec = around appWithFixture $ do
|
|||||||
it "fails with 400 and error" $
|
it "fails with 400 and error" $
|
||||||
post "/simple_pk" "}{ x = 2"
|
post "/simple_pk" "}{ x = 2"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just [json| {"error":"Failed to parse JSON payload. Failed reading: satisfy"} |]
|
matchBody = Just [json| {"message":"Failed to parse JSON payload. Failed reading: satisfy"} |]
|
||||||
, matchStatus = 400
|
, matchStatus = 400
|
||||||
, matchHeaders = []
|
, matchHeaders = []
|
||||||
}
|
}
|
||||||
@@ -94,39 +101,93 @@ spec = around appWithFixture $ do
|
|||||||
|
|
||||||
context "with a fully-specified primary key" $ do
|
context "with a fully-specified primary key" $ do
|
||||||
|
|
||||||
context "with Content-Range header" $
|
|
||||||
it "fails as per RFC7231" $
|
|
||||||
request methodPut "/compound_pk?k1=eq.1&k2=eq.2"
|
|
||||||
[("Content-Range", "0-0")]
|
|
||||||
[json| { "k1":1, "k2":2, "extra":3 } |]
|
|
||||||
`shouldRespondWith` 400
|
|
||||||
|
|
||||||
context "not specifying every column in the table" $
|
context "not specifying every column in the table" $
|
||||||
it "is rejected for lack of idempotence" $
|
it "is rejected for lack of idempotence" $
|
||||||
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||||
[json| { "k1":12, "k2":42 } |]
|
[json| { "k1":12, "k2":42 } |]
|
||||||
`shouldRespondWith` 400
|
`shouldRespondWith` 400
|
||||||
|
|
||||||
context "specifying every column in the table" $
|
context "specifying every column in the table" . after_ (clearTable "compound_pk") $ do
|
||||||
it "succeeds with 201 and link" $ 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 } |]
|
||||||
liftIO $ do
|
liftIO $ do
|
||||||
simpleBody p `shouldBe` ""
|
simpleBody p `shouldBe` ""
|
||||||
simpleStatus p `shouldBe` status200
|
simpleStatus p `shouldBe` status204
|
||||||
|
|
||||||
context "with an auto-incrementing primary key" $
|
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
|
||||||
|
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
|
||||||
|
liftIO $ do
|
||||||
|
length rows `shouldBe` 1
|
||||||
|
let record = head rows
|
||||||
|
compoundK1 record `shouldBe` 12
|
||||||
|
compoundK2 record `shouldBe` 42
|
||||||
|
compoundExtra record `shouldBe` Just 3
|
||||||
|
|
||||||
it "succeeds with 201 and link" $
|
it "can update an existing record" $ do
|
||||||
|
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||||
|
[json| { "k1":12, "k2":42, "extra":4 } |]
|
||||||
|
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
|
||||||
|
[json| { "k1":12, "k2":42, "extra":5 } |]
|
||||||
|
|
||||||
|
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
|
||||||
|
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
|
||||||
|
liftIO $ do
|
||||||
|
length rows `shouldBe` 1
|
||||||
|
let record = head rows
|
||||||
|
compoundExtra record `shouldBe` Just 5
|
||||||
|
|
||||||
|
context "with an auto-incrementing primary key" . after_ (clearTable "auto_incrementing_pk") $
|
||||||
|
|
||||||
|
it "succeeds with 204" $
|
||||||
request methodPut "/auto_incrementing_pk?id=eq.1" []
|
request methodPut "/auto_incrementing_pk?id=eq.1" []
|
||||||
[json| {
|
[json| {
|
||||||
"id":1,
|
"id":1,
|
||||||
"nullable_string":"hi",
|
"nullable_string":"hi",
|
||||||
"non_nullable_string":"bye",
|
"non_nullable_string":"bye",
|
||||||
"inserted_at": "now()"
|
"inserted_at": "2020-11-11"
|
||||||
} |]
|
} |]
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Nothing,
|
matchBody = Nothing,
|
||||||
matchStatus = 200,
|
matchStatus = 204,
|
||||||
matchHeaders = []
|
matchHeaders = []
|
||||||
}
|
}
|
||||||
|
|
||||||
|
describe "Patching record" $ do
|
||||||
|
|
||||||
|
context "to unkonwn uri" $
|
||||||
|
it "gives a 404" $
|
||||||
|
request methodPatch "/fake" []
|
||||||
|
[json| { "real": false } |]
|
||||||
|
`shouldRespondWith` 404
|
||||||
|
|
||||||
|
context "on an empty table" $
|
||||||
|
it "succeeds with no effect" $
|
||||||
|
request methodPatch "/simple_pk" []
|
||||||
|
[json| { "extra":20 } |]
|
||||||
|
`shouldRespondWith` 204
|
||||||
|
|
||||||
|
context "in a nonempty table" . before_ (clearTable "items" >> createItems 15) .
|
||||||
|
after_ (clearTable "items") $ do
|
||||||
|
it "can update a single item" $ do
|
||||||
|
g <- get "/items?id=eq.42"
|
||||||
|
liftIO $ simpleHeaders g
|
||||||
|
`shouldSatisfy` matchHeader "Content-Range" "\\*/0"
|
||||||
|
request methodPatch "/items?id=eq.1" []
|
||||||
|
[json| { "id":42 } |]
|
||||||
|
`shouldRespondWith` 204
|
||||||
|
g' <- get "/items?id=eq.42"
|
||||||
|
liftIO $ simpleHeaders g'
|
||||||
|
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
|
||||||
|
|
||||||
|
it "can update multiple items" $ do
|
||||||
|
replicateM_ 10 $ post "/auto_incrementing_pk"
|
||||||
|
[json| { non_nullable_string: "a" } |]
|
||||||
|
replicateM_ 10 $ post "/auto_incrementing_pk"
|
||||||
|
[json| { non_nullable_string: "b" } |]
|
||||||
|
_ <- request methodPatch
|
||||||
|
"/auto_incrementing_pk?non_nullable_string=eq.a" []
|
||||||
|
[json| { non_nullable_string: "c" } |]
|
||||||
|
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
|
||||||
|
liftIO $ simpleHeaders g
|
||||||
|
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
|
||||||
|
|||||||
+57
-18
@@ -1,45 +1,84 @@
|
|||||||
|
|
||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module Feature.QuerySpec where
|
module Feature.QuerySpec where
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
import Test.Hspec.Wai.JSON
|
||||||
|
import Hasql as H
|
||||||
|
import Hasql.Postgres as H
|
||||||
|
import Control.Monad (void)
|
||||||
|
import Data.Text(Text)
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
|
testSet :: IO ()
|
||||||
|
testSet = do
|
||||||
|
clearTable "items" >> clearTable "no_pk"
|
||||||
|
createItems 15
|
||||||
|
pool <- H.acquirePool pgSettings testPoolOpts
|
||||||
|
void . liftIO $ H.session pool $ H.tx Nothing $ do
|
||||||
|
H.unitEx $ insertNoPk "xyyx" "u"
|
||||||
|
H.unitEx $ insertNoPk "xYYx" "v"
|
||||||
|
|
||||||
|
where
|
||||||
|
insertNoPk :: Text -> Text -> H.Stmt H.Postgres
|
||||||
|
insertNoPk = [H.stmt|insert into "1".no_pk (a, b) values (?,?)|]
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = around appWithFixture $ do
|
spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
|
||||||
describe "Querying a nonexistent table" $
|
describe "Querying a nonexistent table" $
|
||||||
it "causes a 404" $
|
it "causes a 404" $
|
||||||
get "/faketable" `shouldRespondWith` 404
|
get "/faketable" `shouldRespondWith` 404
|
||||||
|
|
||||||
describe "Filtering response" $
|
describe "Filtering response" $ do
|
||||||
context "column equality" $
|
it "matches with equality" $
|
||||||
|
get "/items?id=eq.5"
|
||||||
|
`shouldRespondWith` ResponseMatcher {
|
||||||
|
matchBody = Just [json| [{"id":5}] |]
|
||||||
|
, matchStatus = 200
|
||||||
|
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
||||||
|
}
|
||||||
|
|
||||||
it "matches the predicate" $
|
it "matches items IN" $
|
||||||
get "/items?id=eq.5"
|
get "/items?id=in.1,3,5"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[{\"id\":5}]"
|
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-0/1"]
|
, matchHeaders = ["Content-Range" <:> "0-2/3"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
it "matches with like" $ do
|
||||||
|
get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"}]|]
|
||||||
|
get "/no_pk?a=like.xy*" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"}]|]
|
||||||
|
get "/no_pk?a=like.*YY*" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xYYx","b":"v"}]|]
|
||||||
|
|
||||||
|
it "matches with ilike" $ do
|
||||||
|
get "/no_pk?a=ilike.xy*&order=b.asc" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
|
||||||
|
get "/no_pk?a=ilike.*YY*&order=b.asc" `shouldRespondWith` [json|
|
||||||
|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
|
||||||
|
|
||||||
describe "ordering response" $ do
|
describe "ordering response" $ do
|
||||||
it "by a column asc" $
|
it "by a column asc" $
|
||||||
get "/items?id=lte.2&order=asc.id"
|
get "/items?id=lte.2&order=id.asc"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[{\"id\":1},{\"id\":2}]"
|
matchBody = Just [json| [{"id":1},{"id":2}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||||
}
|
}
|
||||||
it "by a column desc" $
|
it "by a column desc" $
|
||||||
get "/items?id=lte.2&order=desc.id"
|
get "/items?id=lte.2&order=id.desc"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[{\"id\":2},{\"id\":1}]"
|
matchBody = Just [json| [{"id":2},{"id":1}] |]
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
, matchHeaders = ["Content-Range" <:> "0-1/2"]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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"
|
||||||
@@ -50,9 +89,9 @@ spec = around appWithFixture $ do
|
|||||||
}
|
}
|
||||||
|
|
||||||
it "Omits question mark when there are no params" $
|
it "Omits question mark when there are no params" $
|
||||||
get "/no_pk"
|
get "/simple_pk"
|
||||||
`shouldRespondWith` ResponseMatcher {
|
`shouldRespondWith` ResponseMatcher {
|
||||||
matchBody = Just "[]"
|
matchBody = Just "[]"
|
||||||
, matchStatus = 200
|
, matchStatus = 200
|
||||||
, matchHeaders = ["Content-Location" <:> "/no_pk"]
|
, matchHeaders = ["Content-Location" <:> "/simple_pk"]
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module Feature.RangeSpec where
|
module Feature.RangeSpec where
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
@@ -9,7 +8,8 @@ import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
|
|||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = around appWithFixture $
|
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" $
|
||||||
|
|||||||
@@ -1,29 +1,20 @@
|
|||||||
{-# LANGUAGE OverloadedStrings, QuasiQuotes #-}
|
|
||||||
module Feature.StructureSpec where
|
module Feature.StructureSpec where
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec hiding (pendingWith)
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
import Test.Hspec.Wai.JSON
|
import Test.Hspec.Wai.JSON
|
||||||
|
|
||||||
import SpecHelper
|
import SpecHelper
|
||||||
|
|
||||||
import Network.HTTP.Types
|
import Network.HTTP.Types
|
||||||
import Codec.Binary.Base64.String (encode)
|
|
||||||
import Data.Monoid ((<>))
|
|
||||||
import Data.String.Conversions (cs)
|
|
||||||
|
|
||||||
spec :: Spec
|
spec :: Spec
|
||||||
spec = let {uName = "a user"; uPass = "nobody can ever know";
|
spec = around withApp $ do
|
||||||
uRole = "dbapi_test"} in
|
describe "GET /" $ do
|
||||||
around withDatabaseConnection $
|
|
||||||
aroundWith (withUser uName uPass uRole) $ aroundWith withApp $ do
|
|
||||||
describe "GET /" $
|
|
||||||
it "lists views in schema" $
|
it "lists views in schema" $
|
||||||
request methodGet "/"
|
request methodGet "/" [] ""
|
||||||
[("Authorization", "Basic "<>(cs.encode $ cs uName<>":"<>cs uPass))] ""
|
|
||||||
`shouldRespondWith` [json| [
|
`shouldRespondWith` [json| [
|
||||||
{"schema":"1","name":"authors_only","insertable":true}
|
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
|
||||||
, {"schema":"1","name":"auto_incrementing_pk","insertable":true}
|
|
||||||
, {"schema":"1","name":"compound_pk","insertable":true}
|
, {"schema":"1","name":"compound_pk","insertable":true}
|
||||||
, {"schema":"1","name":"has_fk","insertable":true}
|
, {"schema":"1","name":"has_fk","insertable":true}
|
||||||
, {"schema":"1","name":"items","insertable":true}
|
, {"schema":"1","name":"items","insertable":true}
|
||||||
@@ -33,6 +24,17 @@ uRole = "dbapi_test"} in
|
|||||||
] |]
|
] |]
|
||||||
{matchStatus = 200}
|
{matchStatus = 200}
|
||||||
|
|
||||||
|
it "lists only views user has permission to see" $ do
|
||||||
|
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
|
||||||
|
let auth = authHeader "jdoe" "1234"
|
||||||
|
|
||||||
|
request methodGet "/" [auth] ""
|
||||||
|
`shouldRespondWith` [json| [
|
||||||
|
{"schema":"1","name":"authors_only","insertable":true}
|
||||||
|
] |]
|
||||||
|
{matchStatus = 200}
|
||||||
|
|
||||||
|
|
||||||
describe "Table info" $ do
|
describe "Table info" $ do
|
||||||
it "is available with OPTIONS verb" $
|
it "is available with OPTIONS verb" $
|
||||||
request methodOptions "/menagerie" [] "" `shouldRespondWith`
|
request methodOptions "/menagerie" [] "" `shouldRespondWith`
|
||||||
@@ -48,7 +50,7 @@ uRole = "dbapi_test"} in
|
|||||||
"name": "integer",
|
"name": "integer",
|
||||||
"type": "integer",
|
"type": "integer",
|
||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"references": null,
|
"references": null,
|
||||||
@@ -61,7 +63,7 @@ uRole = "dbapi_test"} in
|
|||||||
"name": "double",
|
"name": "double",
|
||||||
"type": "double precision",
|
"type": "double precision",
|
||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"references": null,
|
"references": null,
|
||||||
"position": 2
|
"position": 2
|
||||||
@@ -73,7 +75,7 @@ uRole = "dbapi_test"} in
|
|||||||
"name": "varchar",
|
"name": "varchar",
|
||||||
"type": "character varying",
|
"type": "character varying",
|
||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"position": 3,
|
"position": 3,
|
||||||
"references": null,
|
"references": null,
|
||||||
@@ -86,7 +88,7 @@ uRole = "dbapi_test"} in
|
|||||||
"name": "boolean",
|
"name": "boolean",
|
||||||
"type": "boolean",
|
"type": "boolean",
|
||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"references": null,
|
"references": null,
|
||||||
"position": 4
|
"position": 4
|
||||||
@@ -98,7 +100,7 @@ uRole = "dbapi_test"} in
|
|||||||
"name": "date",
|
"name": "date",
|
||||||
"type": "date",
|
"type": "date",
|
||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"references": null,
|
"references": null,
|
||||||
"position": 5
|
"position": 5
|
||||||
@@ -110,7 +112,7 @@ uRole = "dbapi_test"} in
|
|||||||
"name": "money",
|
"name": "money",
|
||||||
"type": "money",
|
"type": "money",
|
||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"position": 6,
|
"position": 6,
|
||||||
"references": null,
|
"references": null,
|
||||||
@@ -136,9 +138,10 @@ uRole = "dbapi_test"} in
|
|||||||
}
|
}
|
||||||
|]
|
|]
|
||||||
|
|
||||||
it "includes foreign key data" $
|
it "includes foreign key data" $ do
|
||||||
request methodOptions "/has_fk"
|
pendingWith "have to resolve issue #107"
|
||||||
[("Authorization", "Basic "<>(cs.encode $ cs uName<>":"<>cs uPass))] ""
|
|
||||||
|
request methodOptions "/has_fk" [] ""
|
||||||
`shouldRespondWith` [json|
|
`shouldRespondWith` [json|
|
||||||
{
|
{
|
||||||
"pkey": ["id"],
|
"pkey": ["id"],
|
||||||
@@ -153,7 +156,7 @@ uRole = "dbapi_test"} in
|
|||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"nullable": false,
|
"nullable": false,
|
||||||
"position": 1,
|
"position": 1,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"references": null
|
"references": null
|
||||||
}, {
|
}, {
|
||||||
"default": null,
|
"default": null,
|
||||||
@@ -165,7 +168,7 @@ uRole = "dbapi_test"} in
|
|||||||
"maxLen": null,
|
"maxLen": null,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"position": 2,
|
"position": 2,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"references": {"table": "auto_incrementing_pk", "column": "id"}
|
"references": {"table": "auto_incrementing_pk", "column": "id"}
|
||||||
}, {
|
}, {
|
||||||
"default": null,
|
"default": null,
|
||||||
@@ -177,7 +180,7 @@ uRole = "dbapi_test"} in
|
|||||||
"maxLen": 255,
|
"maxLen": 255,
|
||||||
"nullable": true,
|
"nullable": true,
|
||||||
"position": 3,
|
"position": 3,
|
||||||
"enum": null,
|
"enum": [],
|
||||||
"references": {"table": "simple_pk", "column": "k"}
|
"references": {"table": "simple_pk", "column": "k"}
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
|||||||
+2
-11
@@ -1,17 +1,8 @@
|
|||||||
module Main where
|
module Main where
|
||||||
|
|
||||||
import Database.HDBC (runRaw, disconnect)
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
import SpecHelper
|
||||||
import Spec
|
import Spec
|
||||||
import SpecHelper (openConnection, loadFixture)
|
|
||||||
|
|
||||||
main :: IO ()
|
main :: IO ()
|
||||||
main = do
|
main = resetDb >> hspec spec
|
||||||
c <-openConnection
|
|
||||||
runRaw c "drop schema if exists \"1\" cascade"
|
|
||||||
runRaw c "drop schema if exists private cascade"
|
|
||||||
runRaw c "drop schema if exists dbapi cascade"
|
|
||||||
loadFixture "roles" c
|
|
||||||
loadFixture "schema" c
|
|
||||||
disconnect c
|
|
||||||
hspec spec
|
|
||||||
|
|||||||
+71
-42
@@ -1,73 +1,86 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module SpecHelper where
|
module SpecHelper where
|
||||||
|
|
||||||
import Network.Wai
|
import Network.Wai
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
import Test.Hspec.Wai
|
import Test.Hspec.Wai
|
||||||
|
|
||||||
import Database.HDBC
|
import Hasql as H
|
||||||
import Database.HDBC.PostgreSQL
|
import Hasql.Backend 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 Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
|
||||||
hRange, hAuthorization)
|
hRange, hAuthorization)
|
||||||
import Codec.Binary.Base64.String (encode)
|
import Codec.Binary.Base64.String (encode)
|
||||||
import Data.CaseInsensitive (CI(..))
|
import Data.CaseInsensitive (CI(..))
|
||||||
|
import Data.Maybe (fromMaybe)
|
||||||
import Text.Regex.TDFA ((=~))
|
import Text.Regex.TDFA ((=~))
|
||||||
import qualified Data.ByteString.Char8 as BS
|
import qualified Data.ByteString.Char8 as BS
|
||||||
import Network.Wai.Middleware.Cors (cors)
|
import Network.Wai.Middleware.Cors (cors)
|
||||||
|
import System.Process (readProcess)
|
||||||
|
|
||||||
import Middleware(clientErrors, withSavepoint, authenticated)
|
import App (app)
|
||||||
|
import Config (AppConfig(..), corsPolicy)
|
||||||
import Dbapi (app, corsPolicy, AppConfig(..))
|
import Middleware
|
||||||
import PgQuery(addUser)
|
import Error(errResponse)
|
||||||
|
-- import Auth (addUser)
|
||||||
|
|
||||||
isLeft :: Either a b -> Bool
|
isLeft :: Either a b -> Bool
|
||||||
isLeft (Left _ ) = True
|
isLeft (Left _ ) = True
|
||||||
isLeft _ = False
|
isLeft _ = False
|
||||||
|
|
||||||
cfg :: AppConfig
|
cfg :: AppConfig
|
||||||
cfg = AppConfig "postgres://dbapi_test:@localhost:5432/dbapi_test" 9000 "dbapi_anonymous" False
|
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10 "1"
|
||||||
|
|
||||||
openConnection :: IO Connection
|
testPoolOpts :: PoolSettings
|
||||||
openConnection = connectPostgreSQL' $ configDbUri cfg
|
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
|
||||||
|
|
||||||
withDatabaseConnection :: (Connection -> IO ()) -> IO ()
|
pgSettings :: H.Settings
|
||||||
withDatabaseConnection = bracket openConnection disconnect
|
pgSettings = H.ParamSettings (cs $ configDbHost cfg)
|
||||||
|
(fromIntegral $ configDbPort cfg)
|
||||||
|
(cs $ configDbUser cfg)
|
||||||
|
(cs $ configDbPass cfg)
|
||||||
|
(cs $ configDbName cfg)
|
||||||
|
|
||||||
loadFixture :: String -> Connection -> IO ()
|
withApp :: ActionWith Application -> IO ()
|
||||||
loadFixture name conn = do
|
withApp perform = do
|
||||||
sql <- readFile $ "test/fixtures/" ++ name ++ ".sql"
|
let anonRole = cs $ configAnonRole cfg
|
||||||
runRaw conn sql
|
currRole = cs $ configDbUser cfg
|
||||||
|
pool :: H.Pool H.Postgres
|
||||||
|
<- H.acquirePool pgSettings testPoolOpts
|
||||||
|
|
||||||
dbWithSchema :: ActionWith Connection -> IO ()
|
perform $ middle $ \req resp -> do
|
||||||
dbWithSchema action = withDatabaseConnection $ \c -> do
|
body <- strictRequestBody req
|
||||||
runRaw c "begin;"
|
result <- liftIO $ H.session pool $ H.tx Nothing
|
||||||
action c
|
$ authenticated currRole anonRole (app (cs $ configV1Schema cfg) body) req
|
||||||
rollback c
|
either (resp . errResponse) resp result
|
||||||
|
|
||||||
withUser :: BS.ByteString -> BS.ByteString -> BS.ByteString ->
|
where middle = cors corsPolicy
|
||||||
ActionWith Connection -> ActionWith Connection
|
|
||||||
withUser name pass role action conn = do
|
|
||||||
addUser name pass role conn
|
|
||||||
finally (action conn) $ do
|
|
||||||
_ <- run conn "delete from dbapi.auth where id=?" [toSql name]
|
|
||||||
runRaw conn "commit"
|
|
||||||
|
|
||||||
withApp :: ActionWith Application -> ActionWith Connection
|
|
||||||
withApp action conn = do
|
|
||||||
runRaw conn "begin;"
|
|
||||||
action $ cors corsPolicy $ authenticated "dbapi_anonymous" app conn
|
|
||||||
rollback conn
|
|
||||||
|
|
||||||
appWithFixture :: ActionWith Application -> IO ()
|
resetDb :: IO ()
|
||||||
appWithFixture action = withDatabaseConnection $ \c -> do
|
resetDb = do
|
||||||
runRaw c "begin;"
|
pool :: H.Pool H.Postgres
|
||||||
action $ cors corsPolicy . clientErrors $
|
<- H.acquirePool pgSettings testPoolOpts
|
||||||
(authenticated "dbapi_anonymous" . withSavepoint) app c
|
void . liftIO $ H.session pool $
|
||||||
rollback c
|
H.tx Nothing $ do
|
||||||
|
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"
|
||||||
|
|
||||||
|
|
||||||
|
loadFixture :: FilePath -> IO()
|
||||||
|
loadFixture name =
|
||||||
|
void $ readProcess "psql" ["-U", "postgrest_test", "-d", "postgrest_test", "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
|
||||||
|
|
||||||
|
|
||||||
rangeHdrs :: ByteRange -> [Header]
|
rangeHdrs :: ByteRange -> [Header]
|
||||||
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
|
||||||
@@ -80,8 +93,24 @@ matchHeader name valRegex headers =
|
|||||||
maybe False (=~ valRegex) $ lookup name headers
|
maybe False (=~ valRegex) $ lookup name headers
|
||||||
|
|
||||||
authHeader :: String -> String -> Header
|
authHeader :: String -> String -> Header
|
||||||
authHeader user pass =
|
authHeader u p =
|
||||||
(hAuthorization, cs $ "Basic " ++ encode (user ++ ":" ++ pass))
|
(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 ()
|
||||||
|
|||||||
+30
-12
@@ -1,17 +1,16 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module TestTypes (
|
module TestTypes (
|
||||||
IncPK(..),
|
IncPK(..)
|
||||||
fromList
|
, CompoundPK(..)
|
||||||
|
-- , incFromList
|
||||||
|
-- , compoundFromList
|
||||||
) where
|
) where
|
||||||
|
|
||||||
import qualified Data.Aeson as JSON
|
import qualified Data.Aeson as JSON
|
||||||
import Data.Aeson ((.:))
|
import Data.Aeson ((.:))
|
||||||
import Data.Maybe (fromJust)
|
-- import Data.Maybe (fromJust)
|
||||||
import Control.Applicative ((<$>), (<*>))
|
import Control.Applicative ((<$>), (<*>))
|
||||||
import Control.Monad (mzero)
|
import Control.Monad (mzero)
|
||||||
|
|
||||||
import Database.HDBC (SqlValue, fromSql)
|
|
||||||
|
|
||||||
data IncPK = IncPK {
|
data IncPK = IncPK {
|
||||||
incId :: Int
|
incId :: Int
|
||||||
, incNullableStr :: Maybe String
|
, incNullableStr :: Maybe String
|
||||||
@@ -27,9 +26,28 @@ instance JSON.FromJSON IncPK where
|
|||||||
r .: "inserted_at"
|
r .: "inserted_at"
|
||||||
parseJSON _ = mzero
|
parseJSON _ = mzero
|
||||||
|
|
||||||
fromList :: [(String, SqlValue)] -> IncPK
|
-- incFromList :: [(String, SqlValue)] -> IncPK
|
||||||
fromList row = IncPK
|
-- incFromList row = IncPK
|
||||||
(fromSql . fromJust $ lookup "id" row)
|
-- (fromSql . fromJust $ lookup "id" row)
|
||||||
(fromSql . fromJust $ lookup "nullable_string" row)
|
-- (fromSql . fromJust $ lookup "nullable_string" row)
|
||||||
(fromSql . fromJust $ lookup "non_nullable_string" row)
|
-- (fromSql . fromJust $ lookup "non_nullable_string" row)
|
||||||
(fromSql . fromJust $ lookup "inserted_at" row)
|
-- (fromSql . fromJust $ lookup "inserted_at" row)
|
||||||
|
|
||||||
|
data CompoundPK = CompoundPK {
|
||||||
|
compoundK1 :: Int
|
||||||
|
, compoundK2 :: Int
|
||||||
|
, compoundExtra :: Maybe Int
|
||||||
|
}
|
||||||
|
|
||||||
|
instance JSON.FromJSON CompoundPK where
|
||||||
|
parseJSON (JSON.Object r) = CompoundPK <$>
|
||||||
|
r .: "k1" <*>
|
||||||
|
r .: "k2" <*>
|
||||||
|
r .: "extra"
|
||||||
|
parseJSON _ = mzero
|
||||||
|
|
||||||
|
-- compoundFromList :: [(String, SqlValue)] -> CompoundPK
|
||||||
|
-- compoundFromList row = CompoundPK
|
||||||
|
-- (fromSql . fromJust $ lookup "k1" row)
|
||||||
|
-- (fromSql . fromJust $ lookup "k2" row)
|
||||||
|
-- (fromSql . fromJust $ lookup "extra" row)
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
module Unit.ErrorsSpec where
|
|
||||||
|
|
||||||
import Test.Hspec
|
|
||||||
|
|
||||||
import Database.HDBC (runRaw, quickQuery, fromSql, SqlError)
|
|
||||||
import SpecHelper (dbWithSchema)
|
|
||||||
import Middleware (withSavepoint)
|
|
||||||
import PgQuery (insert)
|
|
||||||
import Types(SqlRow(..))
|
|
||||||
import Control.Exception(catch)
|
|
||||||
import Control.Monad(void)
|
|
||||||
import Network.Wai (defaultRequest, responseLBS)
|
|
||||||
import Network.HTTP.Types.Status (ok200)
|
|
||||||
|
|
||||||
spec :: Spec
|
|
||||||
spec = let
|
|
||||||
dbErrApp conn _ res = do
|
|
||||||
putStrLn "In fake app"
|
|
||||||
_ <- insert "1" "items" (SqlRow []) conn
|
|
||||||
runRaw conn "select 1/0"
|
|
||||||
_ <- insert "1" "items" (SqlRow []) conn
|
|
||||||
res $ responseLBS ok200 [("Content-Type", "application/json")] "{}"
|
|
||||||
in around dbWithSchema $
|
|
||||||
|
|
||||||
describe "withSavepoint" $
|
|
||||||
it "allows partial rollback of request" $ \c -> do
|
|
||||||
let app = withSavepoint dbErrApp c
|
|
||||||
[[beforeCount]] <- quickQuery c "select count(*) from \"1\".items" []
|
|
||||||
runRaw c "set role dbapi_anonymous"
|
|
||||||
_ <- insert "1" "items" (SqlRow []) c
|
|
||||||
catch (void $ app defaultRequest (const undefined) ) $
|
|
||||||
\e -> let _ = (e::SqlError) in do
|
|
||||||
_ <- insert "1" "items" (SqlRow []) c
|
|
||||||
[[afterCount]] <- quickQuery c "select count(*) from \"1\".items" []
|
|
||||||
fromSql afterCount `shouldBe` (fromSql beforeCount::Int) + 2
|
|
||||||
@@ -1,15 +1,16 @@
|
|||||||
{-# LANGUAGE OverloadedStrings #-}
|
|
||||||
|
|
||||||
module Unit.PgQuerySpec where
|
module Unit.PgQuerySpec where
|
||||||
|
|
||||||
import Test.Hspec
|
import Test.Hspec
|
||||||
|
import Test.QuickCheck
|
||||||
|
import Test.QuickCheck.Monadic
|
||||||
|
|
||||||
import Database.HDBC (IConnection, SqlValue, toSql, prepare,
|
import Database.HDBC (IConnection, SqlValue, toSql, prepare,
|
||||||
quickQuery, fromSql, execute, seState, fetchAllRowsAL)
|
quickQuery, fromSql, execute, seState, fetchAllRowsAL)
|
||||||
|
|
||||||
import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass)
|
import PgQuery (LoginAttempt(..), insert, addUser, signInRole, checkPass
|
||||||
|
, pgFmtIdent, pgFmtLit)
|
||||||
import Types (SqlRow(SqlRow))
|
import Types (SqlRow(SqlRow))
|
||||||
import TestTypes (fromList, incStr, incNullableStr, incInsert, incId)
|
import TestTypes (incFromList, incStr, incNullableStr, incInsert, incId)
|
||||||
import Data.Map (toList)
|
import Data.Map (toList)
|
||||||
import Data.String.Conversions (cs)
|
import Data.String.Conversions (cs)
|
||||||
import Data.Monoid ((<>))
|
import Data.Monoid ((<>))
|
||||||
@@ -33,13 +34,13 @@ spec = around dbWithSchema $ do
|
|||||||
it "inserts and responds with a full object description" $ \conn -> do
|
it "inserts and responds with a full object description" $ \conn -> do
|
||||||
r <- insert "1" "auto_incrementing_pk" (SqlRow [
|
r <- insert "1" "auto_incrementing_pk" (SqlRow [
|
||||||
("non_nullable_string", toSql ("a string"::String))]) conn
|
("non_nullable_string", toSql ("a string"::String))]) conn
|
||||||
let returnRow = fromList . toList $ r
|
let returnRow = incFromList . toList $ r
|
||||||
incStr returnRow `shouldBe` "a string"
|
incStr returnRow `shouldBe` "a string"
|
||||||
incNullableStr returnRow `shouldBe` Nothing
|
incNullableStr returnRow `shouldBe` Nothing
|
||||||
incInsert returnRow `shouldSatisfy` not . null
|
incInsert returnRow `shouldSatisfy` not . null
|
||||||
incId returnRow `shouldSatisfy` (>= 0)
|
incId returnRow `shouldSatisfy` (>= 0)
|
||||||
tRows <- quickALQuery conn "select * from \"1\".auto_incrementing_pk" []
|
tRows <- quickALQuery conn "select * from \"1\".auto_incrementing_pk" []
|
||||||
[returnRow] `shouldBe` map fromList tRows
|
[returnRow] `shouldBe` map incFromList tRows
|
||||||
|
|
||||||
it "throws an exception if the PK is not unique" $ \conn -> do
|
it "throws an exception if the PK is not unique" $ \conn -> do
|
||||||
r <- insert "1" "auto_incrementing_pk" (SqlRow [
|
r <- insert "1" "auto_incrementing_pk" (SqlRow [
|
||||||
@@ -63,7 +64,7 @@ spec = around dbWithSchema $ do
|
|||||||
describe "addUser" $ do
|
describe "addUser" $ do
|
||||||
it "adds a correct user to the right table" $ \conn -> do
|
it "adds a correct user to the right table" $ \conn -> do
|
||||||
addUser user pass role conn
|
addUser user pass role conn
|
||||||
[r] <- quickQuery conn "select * from dbapi.auth" []
|
[r] <- quickQuery conn "select * from postgrest.auth" []
|
||||||
let [newUser, newRole, encryptedPass] = map fromSql r :: [String]
|
let [newUser, newRole, encryptedPass] = map fromSql r :: [String]
|
||||||
cs newUser `shouldBe` user
|
cs newUser `shouldBe` user
|
||||||
cs newRole `shouldBe` role
|
cs newRole `shouldBe` role
|
||||||
@@ -83,3 +84,16 @@ spec = around dbWithSchema $ do
|
|||||||
it "returns nothing with bad creds" $ \conn -> do
|
it "returns nothing with bad creds" $ \conn -> do
|
||||||
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
|
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
|
||||||
signInRole user (pass <> "crap") conn `shouldReturn` LoginFailed
|
signInRole user (pass <> "crap") conn `shouldReturn` LoginFailed
|
||||||
|
|
||||||
|
describe "pgFmtIdent" $
|
||||||
|
it "Does what format %I would do" $ \conn -> property $ \fuzz ->
|
||||||
|
monadicIO $ do
|
||||||
|
[[row]] <- run $ quickALQuery conn "select format('%I', ? :: varchar)" [toSql (fuzz :: String)]
|
||||||
|
assert $ fromSql (snd row) == pgFmtIdent (cs fuzz)
|
||||||
|
|
||||||
|
describe "pgFmtLit" $
|
||||||
|
it "Does what format %L would do" $ \conn ->
|
||||||
|
property $ monadicIO $ do
|
||||||
|
fuzz <- pick arbitrary
|
||||||
|
[[row]] <- run $ quickALQuery conn "select format('%L', ? :: varchar)" [toSql (fuzz :: String)]
|
||||||
|
assert $ fromSql (snd row) == pgFmtLit (cs fuzz)
|
||||||
@@ -34,4 +34,4 @@ spec = around dbWithSchema $ beforeWith setRole $ do
|
|||||||
("auto_inc_fk", ForeignKey {fkTable="auto_incrementing_pk", fkCol="id"}),
|
("auto_inc_fk", ForeignKey {fkTable="auto_incrementing_pk", fkCol="id"}),
|
||||||
("simple_fk", ForeignKey { fkTable="simple_pk", fkCol="k"})]
|
("simple_fk", ForeignKey { fkTable="simple_pk", fkCol="k"})]
|
||||||
|
|
||||||
where setRole conn = quickQuery conn "set role dbapi_test" [] >> return conn
|
where setRole conn = quickQuery conn "set role postgrest_test" [] >> return conn
|
||||||
Vendored
+1
-1
@@ -1 +1 @@
|
|||||||
pg_dump --host localhost --port 5432 --username "postgres" --no-password --format plain --column-inserts --verbose --file "./test/fixtures/schema.sql" "dbapi_test"
|
pg_dump --host localhost --port 5432 --username "postgres" --no-password --format plain --column-inserts --verbose --file "./test/fixtures/schema.sql" "postgrest_test"
|
||||||
|
|||||||
Vendored
+3
-6
@@ -11,9 +11,6 @@ BEGIN
|
|||||||
END;
|
END;
|
||||||
$$;
|
$$;
|
||||||
|
|
||||||
select pg_temp.create_role_if_not_exists('dbapi_anonymous', 'with nologin');
|
select pg_temp.create_role_if_not_exists('postgrest_anonymous', 'with nologin') as a
|
||||||
select pg_temp.create_role_if_not_exists('test_default_role', 'with nologin');
|
, pg_temp.create_role_if_not_exists('test_default_role', 'with nologin') as b
|
||||||
|
, pg_temp.create_role_if_not_exists('postgrest_test_author', 'with nologin') into temp shh;
|
||||||
select pg_temp.create_role_if_not_exists('dbapi_test_author', 'with nologin');
|
|
||||||
select pg_temp.create_role_if_not_exists('dbapi_test_author_a', 'with nologin in role dbapi_test_author');
|
|
||||||
select pg_temp.create_role_if_not_exists('dbapi_test_author_b', 'with nologin in role dbapi_test_author');
|
|
||||||
|
|||||||
Vendored
+334
-189
@@ -2,50 +2,48 @@
|
|||||||
-- PostgreSQL database dump
|
-- PostgreSQL database dump
|
||||||
--
|
--
|
||||||
|
|
||||||
-- Dumped from database version 9.3.4
|
-- Dumped from database version 9.3.5
|
||||||
-- Dumped by pg_dump version 9.3.4
|
-- Dumped by pg_dump version 9.3.5
|
||||||
-- Started on 2014-10-01 13:41:39 PDT
|
-- Started on 2014-10-21 15:12:44 PDT
|
||||||
-- Started on 2014-10-07 16:46:34 PDT
|
|
||||||
|
|
||||||
SET statement_timeout = 0;
|
SET statement_timeout = 0;
|
||||||
SET lock_timeout = 0;
|
|
||||||
SET client_encoding = 'UTF8';
|
SET client_encoding = 'UTF8';
|
||||||
SET standard_conforming_strings = on;
|
SET standard_conforming_strings = on;
|
||||||
SET check_function_bodies = false;
|
SET check_function_bodies = false;
|
||||||
SET client_min_messages = warning;
|
SET client_min_messages = warning;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 11 (class 2615 OID 280932)
|
-- TOC entry 20 (class 2615 OID 337536)
|
||||||
-- Name: 1; Type: SCHEMA; Schema: -; Owner: dbapi_test
|
-- Name: 1; Type: SCHEMA; Schema: -; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SCHEMA "1";
|
CREATE SCHEMA "1";
|
||||||
|
|
||||||
|
|
||||||
ALTER SCHEMA "1" OWNER TO dbapi_test;
|
ALTER SCHEMA "1" OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 8 (class 2615 OID 50928)
|
-- TOC entry 19 (class 2615 OID 337537)
|
||||||
-- Name: dbapi; Type: SCHEMA; Schema: -; Owner: dbapi_test
|
-- Name: postgrest; Type: SCHEMA; Schema: -; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SCHEMA dbapi;
|
CREATE SCHEMA postgrest;
|
||||||
|
|
||||||
|
|
||||||
ALTER SCHEMA dbapi OWNER TO dbapi_test;
|
ALTER SCHEMA postgrest OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 9 (class 2615 OID 50929)
|
-- TOC entry 16 (class 2615 OID 337538)
|
||||||
-- Name: private; Type: SCHEMA; Schema: -; Owner: dbapi_test
|
-- Name: private; Type: SCHEMA; Schema: -; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SCHEMA private;
|
CREATE SCHEMA private;
|
||||||
|
|
||||||
|
|
||||||
ALTER SCHEMA private OWNER TO dbapi_test;
|
ALTER SCHEMA private OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 187 (class 3079 OID 12018)
|
-- TOC entry 205 (class 3079 OID 12018)
|
||||||
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
|
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner:
|
||||||
--
|
--
|
||||||
|
|
||||||
@@ -53,29 +51,37 @@ CREATE EXTENSION IF NOT EXISTS plpgsql WITH SCHEMA pg_catalog;
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
-- TOC entry 2321 (class 0 OID 0)
|
||||||
|
-- Dependencies: 205
|
||||||
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
|
-- Name: EXTENSION plpgsql; Type: COMMENT; Schema: -; Owner:
|
||||||
--
|
--
|
||||||
|
|
||||||
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
|
COMMENT ON EXTENSION plpgsql IS 'PL/pgSQL procedural language';
|
||||||
|
|
||||||
|
|
||||||
SET search_path = "1", pg_catalog;
|
SET search_path = "1", pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 571 (class 1247 OID 309112)
|
-- TOC entry 553 (class 1247 OID 337540)
|
||||||
-- Name: enum_menagerie_type; Type: TYPE; Schema: 1; Owner: postgres
|
-- Name: enum_menagerie_type; Type: TYPE; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TYPE "1".enum_menagerie_type AS ENUM (
|
CREATE TYPE enum_menagerie_type AS ENUM (
|
||||||
'foo',
|
'foo',
|
||||||
'bar'
|
'bar'
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE "1".enum_menagerie_type OWNER TO postgrest_test;
|
||||||
|
|
||||||
|
SET search_path = postgrest, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: check_role_exists(); Type: FUNCTION; Schema: dbapi; Owner: dbapi_test
|
-- TOC entry 218 (class 1255 OID 337545)
|
||||||
|
-- Name: check_role_exists(); Type: FUNCTION; Schema: postgrest; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE FUNCTION dbapi.check_role_exists() RETURNS trigger
|
CREATE FUNCTION check_role_exists() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
begin
|
begin
|
||||||
@@ -88,10 +94,14 @@ end
|
|||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
||||||
ALTER FUNCTION dbapi.check_role_exists() OWNER TO dbapi_test;
|
ALTER FUNCTION postgrest.check_role_exists() OWNER TO postgrest_test;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 219 (class 1255 OID 337546)
|
||||||
|
-- Name: update_owner(); Type: FUNCTION; Schema: postgrest; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
CREATE FUNCTION dbapi.update_owner() RETURNS trigger
|
CREATE FUNCTION update_owner() RETURNS trigger
|
||||||
LANGUAGE plpgsql
|
LANGUAGE plpgsql
|
||||||
AS $$
|
AS $$
|
||||||
BEGIN
|
BEGIN
|
||||||
@@ -101,7 +111,7 @@ END;
|
|||||||
$$;
|
$$;
|
||||||
|
|
||||||
|
|
||||||
ALTER FUNCTION dbapi.update_owner() OWNER TO dbapi_test;
|
ALTER FUNCTION postgrest.update_owner() OWNER TO postgrest_test;
|
||||||
|
|
||||||
SET search_path = "1", pg_catalog;
|
SET search_path = "1", pg_catalog;
|
||||||
|
|
||||||
@@ -109,6 +119,9 @@ SET default_tablespace = '';
|
|||||||
|
|
||||||
SET default_with_oids = false;
|
SET default_with_oids = false;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 191 (class 1259 OID 337547)
|
||||||
|
-- Name: authors_only; Type: TABLE; Schema: 1; Owner: postgrest_test_author; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE authors_only (
|
CREATE TABLE authors_only (
|
||||||
@@ -116,10 +129,11 @@ CREATE TABLE authors_only (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".authors_only OWNER TO dbapi_test_author;
|
ALTER TABLE "1".authors_only OWNER TO postgrest_test_author;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 175 (class 1259 OID 280937)
|
-- TOC entry 192 (class 1259 OID 337553)
|
||||||
|
-- Name: auto_incrementing_pk; Type: TABLE; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE auto_incrementing_pk (
|
CREATE TABLE auto_incrementing_pk (
|
||||||
@@ -130,10 +144,11 @@ CREATE TABLE auto_incrementing_pk (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".auto_incrementing_pk OWNER TO dbapi_test;
|
ALTER TABLE "1".auto_incrementing_pk OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE; Schema: 1; Owner: dbapi_test
|
-- TOC entry 193 (class 1259 OID 337560)
|
||||||
|
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SEQUENCE auto_incrementing_pk_id_seq
|
CREATE SEQUENCE auto_incrementing_pk_id_seq
|
||||||
@@ -144,16 +159,20 @@ CREATE SEQUENCE auto_incrementing_pk_id_seq
|
|||||||
CACHE 1;
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".auto_incrementing_pk_id_seq OWNER TO dbapi_test;
|
ALTER TABLE "1".auto_incrementing_pk_id_seq OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
|
-- TOC entry 2324 (class 0 OID 0)
|
||||||
|
-- Dependencies: 193
|
||||||
|
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE OWNED BY; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER SEQUENCE auto_incrementing_pk_id_seq OWNED BY auto_incrementing_pk.id;
|
ALTER SEQUENCE auto_incrementing_pk_id_seq OWNED BY auto_incrementing_pk.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: compound_pk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
|
-- TOC entry 194 (class 1259 OID 337562)
|
||||||
|
-- Name: compound_pk; Type: TABLE; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE compound_pk (
|
CREATE TABLE compound_pk (
|
||||||
@@ -163,10 +182,11 @@ CREATE TABLE compound_pk (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".compound_pk OWNER TO dbapi_test;
|
ALTER TABLE "1".compound_pk OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: has_fk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
|
-- TOC entry 195 (class 1259 OID 337565)
|
||||||
|
-- Name: has_fk; Type: TABLE; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE has_fk (
|
CREATE TABLE has_fk (
|
||||||
@@ -176,11 +196,11 @@ CREATE TABLE has_fk (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".has_fk OWNER TO dbapi_test;
|
ALTER TABLE "1".has_fk OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 185 (class 1259 OID 50947)
|
-- TOC entry 196 (class 1259 OID 337568)
|
||||||
-- Name: has_fk_id_seq; Type: SEQUENCE; Schema: 1; Owner: dbapi_test
|
-- Name: has_fk_id_seq; Type: SEQUENCE; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SEQUENCE has_fk_id_seq
|
CREATE SEQUENCE has_fk_id_seq
|
||||||
@@ -191,20 +211,20 @@ CREATE SEQUENCE has_fk_id_seq
|
|||||||
CACHE 1;
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".has_fk_id_seq OWNER TO dbapi_test;
|
ALTER TABLE "1".has_fk_id_seq OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2042 (class 0 OID 0)
|
-- TOC entry 2327 (class 0 OID 0)
|
||||||
-- Dependencies: 185
|
-- Dependencies: 196
|
||||||
-- Name: has_fk_id_seq; Type: SEQUENCE OWNED BY; Schema: 1; Owner: dbapi_test
|
-- Name: has_fk_id_seq; Type: SEQUENCE OWNED BY; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER SEQUENCE has_fk_id_seq OWNED BY has_fk.id;
|
ALTER SEQUENCE has_fk_id_seq OWNED BY has_fk.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 186 (class 1259 OID 50949)
|
-- TOC entry 197 (class 1259 OID 337570)
|
||||||
-- Name: items; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
|
-- Name: items; Type: TABLE; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE items (
|
CREATE TABLE items (
|
||||||
@@ -212,10 +232,11 @@ CREATE TABLE items (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".items OWNER TO dbapi_test;
|
ALTER TABLE "1".items OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: items_id_seq; Type: SEQUENCE; Schema: 1; Owner: dbapi_test
|
-- TOC entry 198 (class 1259 OID 337573)
|
||||||
|
-- Name: items_id_seq; Type: SEQUENCE; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SEQUENCE items_id_seq
|
CREATE SEQUENCE items_id_seq
|
||||||
@@ -226,17 +247,20 @@ CREATE SEQUENCE items_id_seq
|
|||||||
CACHE 1;
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".items_id_seq OWNER TO dbapi_test;
|
ALTER TABLE "1".items_id_seq OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: items_id_seq; Type: SEQUENCE OWNED BY; Schema: 1; Owner: dbapi_test
|
-- TOC entry 2329 (class 0 OID 0)
|
||||||
|
-- Dependencies: 198
|
||||||
|
-- Name: items_id_seq; Type: SEQUENCE OWNED BY; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER SEQUENCE items_id_seq OWNED BY items.id;
|
ALTER SEQUENCE items_id_seq OWNED BY items.id;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: menagerie; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
|
-- TOC entry 199 (class 1259 OID 337575)
|
||||||
|
-- Name: menagerie; Type: TABLE; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE menagerie (
|
CREATE TABLE menagerie (
|
||||||
@@ -246,14 +270,15 @@ CREATE TABLE menagerie (
|
|||||||
"boolean" boolean NOT NULL,
|
"boolean" boolean NOT NULL,
|
||||||
date date NOT NULL,
|
date date NOT NULL,
|
||||||
money money NOT NULL,
|
money money NOT NULL,
|
||||||
enum "1".enum_menagerie_type not null
|
enum enum_menagerie_type NOT NULL
|
||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".menagerie OWNER TO dbapi_test;
|
ALTER TABLE "1".menagerie OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: no_pk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
|
-- TOC entry 200 (class 1259 OID 337581)
|
||||||
|
-- Name: no_pk; Type: TABLE; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE no_pk (
|
CREATE TABLE no_pk (
|
||||||
@@ -262,10 +287,11 @@ CREATE TABLE no_pk (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".no_pk OWNER TO dbapi_test;
|
ALTER TABLE "1".no_pk OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: simple_pk; Type: TABLE; Schema: 1; Owner: dbapi_test; Tablespace:
|
-- TOC entry 201 (class 1259 OID 337587)
|
||||||
|
-- Name: simple_pk; Type: TABLE; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE simple_pk (
|
CREATE TABLE simple_pk (
|
||||||
@@ -274,12 +300,13 @@ CREATE TABLE simple_pk (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE "1".simple_pk OWNER TO dbapi_test;
|
ALTER TABLE "1".simple_pk OWNER TO postgrest_test;
|
||||||
|
|
||||||
SET search_path = dbapi, pg_catalog;
|
SET search_path = postgrest, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: auth; Type: TABLE; Schema: dbapi; Owner: dbapi_test; Tablespace:
|
-- TOC entry 202 (class 1259 OID 337593)
|
||||||
|
-- Name: auth; Type: TABLE; Schema: postgrest; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE auth (
|
CREATE TABLE auth (
|
||||||
@@ -289,12 +316,13 @@ CREATE TABLE auth (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE dbapi.auth OWNER TO dbapi_test;
|
ALTER TABLE postgrest.auth OWNER TO postgrest_test;
|
||||||
|
|
||||||
SET search_path = private, pg_catalog;
|
SET search_path = private, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: articles; Type: TABLE; Schema: private; Owner: dbapi_test; Tablespace:
|
-- TOC entry 203 (class 1259 OID 337599)
|
||||||
|
-- Name: articles; Type: TABLE; Schema: private; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TABLE articles (
|
CREATE TABLE articles (
|
||||||
@@ -304,10 +332,11 @@ CREATE TABLE articles (
|
|||||||
);
|
);
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE private.articles OWNER TO dbapi_test;
|
ALTER TABLE private.articles OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: articles_id_seq; Type: SEQUENCE; Schema: private; Owner: dbapi_test
|
-- TOC entry 204 (class 1259 OID 337605)
|
||||||
|
-- Name: articles_id_seq; Type: SEQUENCE; Schema: private; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE SEQUENCE articles_id_seq
|
CREATE SEQUENCE articles_id_seq
|
||||||
@@ -318,10 +347,12 @@ CREATE SEQUENCE articles_id_seq
|
|||||||
CACHE 1;
|
CACHE 1;
|
||||||
|
|
||||||
|
|
||||||
ALTER TABLE private.articles_id_seq OWNER TO dbapi_test;
|
ALTER TABLE private.articles_id_seq OWNER TO postgrest_test;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: articles_id_seq; Type: SEQUENCE OWNED BY; Schema: private; Owner: dbapi_test
|
-- TOC entry 2336 (class 0 OID 0)
|
||||||
|
-- Dependencies: 204
|
||||||
|
-- Name: articles_id_seq; Type: SEQUENCE OWNED BY; Schema: private; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER SEQUENCE articles_id_seq OWNED BY articles.id;
|
ALTER SEQUENCE articles_id_seq OWNED BY articles.id;
|
||||||
@@ -330,23 +361,24 @@ ALTER SEQUENCE articles_id_seq OWNED BY articles.id;
|
|||||||
SET search_path = "1", pg_catalog;
|
SET search_path = "1", pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: id; Type: DEFAULT; Schema: 1; Owner: dbapi_test
|
-- TOC entry 2165 (class 2604 OID 337607)
|
||||||
|
-- Name: id; Type: DEFAULT; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY auto_incrementing_pk ALTER COLUMN id SET DEFAULT nextval('auto_incrementing_pk_id_seq'::regclass);
|
ALTER TABLE ONLY auto_incrementing_pk ALTER COLUMN id SET DEFAULT nextval('auto_incrementing_pk_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 1886 (class 2604 OID 50987)
|
-- TOC entry 2166 (class 2604 OID 337608)
|
||||||
-- Name: id; Type: DEFAULT; Schema: 1; Owner: dbapi_test
|
-- Name: id; Type: DEFAULT; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass);
|
ALTER TABLE ONLY has_fk ALTER COLUMN id SET DEFAULT nextval('has_fk_id_seq'::regclass);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 1887 (class 2604 OID 50988)
|
-- TOC entry 2167 (class 2604 OID 337609)
|
||||||
-- Name: id; Type: DEFAULT; Schema: 1; Owner: dbapi_test
|
-- Name: id; Type: DEFAULT; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass);
|
ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regclass);
|
||||||
@@ -355,7 +387,8 @@ ALTER TABLE ONLY items ALTER COLUMN id SET DEFAULT nextval('items_id_seq'::regcl
|
|||||||
SET search_path = private, pg_catalog;
|
SET search_path = private, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: id; Type: DEFAULT; Schema: private; Owner: dbapi_test
|
-- TOC entry 2168 (class 2604 OID 337610)
|
||||||
|
-- Name: id; Type: DEFAULT; Schema: private; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY articles ALTER COLUMN id SET DEFAULT nextval('articles_id_seq'::regclass);
|
ALTER TABLE ONLY articles ALTER COLUMN id SET DEFAULT nextval('articles_id_seq'::regclass);
|
||||||
@@ -364,105 +397,101 @@ ALTER TABLE ONLY articles ALTER COLUMN id SET DEFAULT nextval('articles_id_seq':
|
|||||||
SET search_path = "1", pg_catalog;
|
SET search_path = "1", pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2279 (class 0 OID 281006)
|
-- TOC entry 2298 (class 0 OID 337547)
|
||||||
-- Dependencies: 186
|
-- Dependencies: 191
|
||||||
-- Data for Name: authors_only; Type: TABLE DATA; Schema: 1; Owner: dbapi_test_author
|
-- Data for Name: authors_only; Type: TABLE DATA; Schema: 1; Owner: postgrest_test_author
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2268 (class 0 OID 280937)
|
-- TOC entry 2299 (class 0 OID 337553)
|
||||||
-- Dependencies: 175
|
-- Dependencies: 192
|
||||||
-- TOC entry 2016 (class 0 OID 50932)
|
-- Data for Name: auto_incrementing_pk; Type: TABLE DATA; Schema: 1; Owner: postgrest_test
|
||||||
-- Dependencies: 181
|
|
||||||
-- Data for Name: auto_incrementing_pk; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
|
|
||||||
--
|
|
||||||
|
|
||||||
|
|
||||||
-- TOC entry 2270 (class 0 OID 280946)
|
|
||||||
-- Dependencies: 177
|
|
||||||
-- TOC entry 2051 (class 0 OID 0)
|
|
||||||
-- Dependencies: 182
|
|
||||||
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: dbapi_test
|
|
||||||
--
|
|
||||||
|
|
||||||
SELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 50, true);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- TOC entry 2018 (class 0 OID 50941)
|
|
||||||
-- Dependencies: 183
|
|
||||||
-- Data for Name: compound_pk; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
|
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2271 (class 0 OID 280949)
|
-- TOC entry 2337 (class 0 OID 0)
|
||||||
-- Dependencies: 178
|
-- Dependencies: 193
|
||||||
|
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
SELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 54, true);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2301 (class 0 OID 337562)
|
||||||
|
-- Dependencies: 194
|
||||||
|
-- Data for Name: compound_pk; Type: TABLE DATA; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2052 (class 0 OID 0)
|
-- TOC entry 2302 (class 0 OID 337565)
|
||||||
-- Dependencies: 185
|
-- Dependencies: 195
|
||||||
-- Name: has_fk_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: dbapi_test
|
-- Data for Name: has_fk; Type: TABLE DATA; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2338 (class 0 OID 0)
|
||||||
|
-- Dependencies: 196
|
||||||
|
-- Name: has_fk_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
SELECT pg_catalog.setval('has_fk_id_seq', 1, false);
|
SELECT pg_catalog.setval('has_fk_id_seq', 1, false);
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2021 (class 0 OID 50949)
|
-- TOC entry 2304 (class 0 OID 337570)
|
||||||
-- Dependencies: 186
|
-- Dependencies: 197
|
||||||
-- Data for Name: items; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
|
-- Data for Name: items; Type: TABLE DATA; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
INSERT INTO items (id) VALUES (1);
|
INSERT INTO items (id) VALUES (1);
|
||||||
INSERT INTO items (id) VALUES (2);
|
--
|
||||||
INSERT INTO items (id) VALUES (3);
|
-- TOC entry 2339 (class 0 OID 0)
|
||||||
INSERT INTO items (id) VALUES (4);
|
-- Dependencies: 198
|
||||||
INSERT INTO items (id) VALUES (5);
|
-- Name: items_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: postgrest_test
|
||||||
INSERT INTO items (id) VALUES (6);
|
--
|
||||||
INSERT INTO items (id) VALUES (7);
|
|
||||||
INSERT INTO items (id) VALUES (8);
|
SELECT pg_catalog.setval('items_id_seq', 19, true);
|
||||||
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);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: items_id_seq; Type: SEQUENCE SET; Schema: 1; Owner: dbapi_test
|
-- TOC entry 2306 (class 0 OID 337575)
|
||||||
--
|
-- Dependencies: 199
|
||||||
|
-- Data for Name: menagerie; Type: TABLE DATA; Schema: 1; Owner: postgrest_test
|
||||||
SELECT pg_catalog.setval('items_id_seq', 15, true);
|
|
||||||
|
|
||||||
|
|
||||||
--
|
|
||||||
-- Data for Name: menagerie; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
|
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: no_pk; Type: TABLE DATA; Schema: 1; Owner: dbapi_test
|
-- TOC entry 2307 (class 0 OID 337581)
|
||||||
|
-- Dependencies: 200
|
||||||
|
-- Data for Name: no_pk; Type: TABLE DATA; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2308 (class 0 OID 337587)
|
||||||
|
-- Dependencies: 201
|
||||||
|
-- Data for Name: simple_pk; Type: TABLE DATA; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
SET search_path = dbapi, pg_catalog;
|
SET search_path = postgrest, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Data for Name: auth; Type: TABLE DATA; Schema: dbapi; Owner: dbapi_test
|
-- TOC entry 2309 (class 0 OID 337593)
|
||||||
|
-- Dependencies: 202
|
||||||
|
-- Data for Name: auth; Type: TABLE DATA; Schema: postgrest; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
@@ -470,12 +499,17 @@ SET search_path = dbapi, pg_catalog;
|
|||||||
SET search_path = private, pg_catalog;
|
SET search_path = private, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
|
-- TOC entry 2310 (class 0 OID 337599)
|
||||||
|
-- Dependencies: 203
|
||||||
|
-- Data for Name: articles; Type: TABLE DATA; Schema: private; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: articles_id_seq; Type: SEQUENCE SET; Schema: private; Owner: dbapi_test
|
-- TOC entry 2340 (class 0 OID 0)
|
||||||
|
-- Dependencies: 204
|
||||||
|
-- Name: articles_id_seq; Type: SEQUENCE SET; Schema: private; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
SELECT pg_catalog.setval('articles_id_seq', 1, false);
|
SELECT pg_catalog.setval('articles_id_seq', 1, false);
|
||||||
@@ -484,7 +518,8 @@ SELECT pg_catalog.setval('articles_id_seq', 1, false);
|
|||||||
SET search_path = "1", pg_catalog;
|
SET search_path = "1", pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: authors_only_pkey; Type: CONSTRAINT; Schema: 1; Owner: dbapi_test_author; Tablespace:
|
-- TOC entry 2170 (class 2606 OID 337612)
|
||||||
|
-- Name: authors_only_pkey; Type: CONSTRAINT; Schema: 1; Owner: postgrest_test_author; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY authors_only
|
ALTER TABLE ONLY authors_only
|
||||||
@@ -492,7 +527,8 @@ ALTER TABLE ONLY authors_only
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2144 (class 2606 OID 280990)
|
-- TOC entry 2172 (class 2606 OID 337614)
|
||||||
|
-- Name: auto_incrementing_pk_pkey; Type: CONSTRAINT; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY auto_incrementing_pk
|
ALTER TABLE ONLY auto_incrementing_pk
|
||||||
@@ -500,7 +536,8 @@ ALTER TABLE ONLY auto_incrementing_pk
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: compound_pk_pkey; Type: CONSTRAINT; Schema: 1; Owner: dbapi_test; Tablespace:
|
-- TOC entry 2174 (class 2606 OID 337616)
|
||||||
|
-- Name: compound_pk_pkey; Type: CONSTRAINT; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY compound_pk
|
ALTER TABLE ONLY compound_pk
|
||||||
@@ -508,7 +545,8 @@ ALTER TABLE ONLY compound_pk
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 1900 (class 2606 OID 50995)
|
-- TOC entry 2182 (class 2606 OID 337618)
|
||||||
|
-- Name: contacts_pkey; Type: CONSTRAINT; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY simple_pk
|
ALTER TABLE ONLY simple_pk
|
||||||
@@ -516,6 +554,8 @@ ALTER TABLE ONLY simple_pk
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
-- TOC entry 2176 (class 2606 OID 337620)
|
||||||
|
-- Name: has_fk_pkey; Type: CONSTRAINT; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY has_fk
|
ALTER TABLE ONLY has_fk
|
||||||
@@ -523,7 +563,8 @@ ALTER TABLE ONLY has_fk
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 1896 (class 2606 OID 50999)
|
-- TOC entry 2178 (class 2606 OID 337622)
|
||||||
|
-- Name: items_pkey; Type: CONSTRAINT; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY items
|
ALTER TABLE ONLY items
|
||||||
@@ -531,16 +572,19 @@ ALTER TABLE ONLY items
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
-- TOC entry 2180 (class 2606 OID 337624)
|
||||||
|
-- Name: menagerie_pkey; Type: CONSTRAINT; Schema: 1; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY menagerie
|
ALTER TABLE ONLY menagerie
|
||||||
ADD CONSTRAINT menagerie_pkey PRIMARY KEY ("integer");
|
ADD CONSTRAINT menagerie_pkey PRIMARY KEY ("integer");
|
||||||
|
|
||||||
|
|
||||||
SET search_path = dbapi, pg_catalog;
|
SET search_path = postgrest, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: auth_pkey; Type: CONSTRAINT; Schema: dbapi; Owner: dbapi_test; Tablespace:
|
-- TOC entry 2184 (class 2606 OID 337626)
|
||||||
|
-- Name: auth_pkey; Type: CONSTRAINT; Schema: postgrest; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY auth
|
ALTER TABLE ONLY auth
|
||||||
@@ -550,16 +594,19 @@ ALTER TABLE ONLY auth
|
|||||||
SET search_path = private, pg_catalog;
|
SET search_path = private, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
|
-- TOC entry 2186 (class 2606 OID 337628)
|
||||||
|
-- Name: articles_pkey; Type: CONSTRAINT; Schema: private; Owner: postgrest_test; Tablespace:
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY articles
|
ALTER TABLE ONLY articles
|
||||||
ADD CONSTRAINT articles_pkey PRIMARY KEY (id);
|
ADD CONSTRAINT articles_pkey PRIMARY KEY (id);
|
||||||
|
|
||||||
|
|
||||||
SET search_path = dbapi, pg_catalog;
|
SET search_path = postgrest, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: ensure_auth_role_exists; Type: TRIGGER; Schema: dbapi; Owner: dbapi_test
|
-- TOC entry 2189 (class 2620 OID 337630)
|
||||||
|
-- Name: ensure_auth_role_exists; Type: TRIGGER; Schema: postgrest; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE CONSTRAINT TRIGGER ensure_auth_role_exists AFTER INSERT OR UPDATE ON auth NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE check_role_exists();
|
CREATE CONSTRAINT TRIGGER ensure_auth_role_exists AFTER INSERT OR UPDATE ON auth NOT DEFERRABLE INITIALLY IMMEDIATE FOR EACH ROW EXECUTE PROCEDURE check_role_exists();
|
||||||
@@ -568,17 +615,18 @@ CREATE CONSTRAINT TRIGGER ensure_auth_role_exists AFTER INSERT OR UPDATE ON auth
|
|||||||
SET search_path = private, pg_catalog;
|
SET search_path = private, pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- Name: articles_owner_track; Type: TRIGGER; Schema: private; Owner: dbapi_test
|
-- TOC entry 2190 (class 2620 OID 337631)
|
||||||
|
-- Name: articles_owner_track; Type: TRIGGER; Schema: private; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
CREATE TRIGGER articles_owner_track BEFORE INSERT OR UPDATE ON articles FOR EACH ROW EXECUTE PROCEDURE dbapi.update_owner();
|
CREATE TRIGGER articles_owner_track BEFORE INSERT OR UPDATE ON articles FOR EACH ROW EXECUTE PROCEDURE postgrest.update_owner();
|
||||||
|
|
||||||
|
|
||||||
SET search_path = "1", pg_catalog;
|
SET search_path = "1", pg_catalog;
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 1905 (class 2606 OID 51009)
|
-- TOC entry 2187 (class 2606 OID 337632)
|
||||||
-- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: 1; Owner: dbapi_test
|
-- Name: has_fk_fk_fkey; Type: FK CONSTRAINT; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY has_fk
|
ALTER TABLE ONLY has_fk
|
||||||
@@ -586,95 +634,192 @@ ALTER TABLE ONLY has_fk
|
|||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 1906 (class 2606 OID 51015)
|
-- TOC entry 2188 (class 2606 OID 337637)
|
||||||
-- Name: has_fk_simple_fk_fkey; Type: FK CONSTRAINT; Schema: 1; Owner: dbapi_test
|
-- Name: has_fk_simple_fk_fkey; Type: FK CONSTRAINT; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
ALTER TABLE ONLY has_fk
|
ALTER TABLE ONLY has_fk
|
||||||
ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k);
|
ADD CONSTRAINT has_fk_simple_fk_fkey FOREIGN KEY (simple_fk) REFERENCES simple_pk(k);
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2317 (class 0 OID 0)
|
||||||
|
-- Dependencies: 20
|
||||||
|
-- Name: 1; Type: ACL; Schema: -; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
REVOKE ALL ON SCHEMA "1" FROM dbapi_test;
|
REVOKE ALL ON SCHEMA "1" FROM PUBLIC;
|
||||||
GRANT ALL ON SCHEMA "1" TO dbapi_test;
|
REVOKE ALL ON SCHEMA "1" FROM postgrest_test;
|
||||||
GRANT USAGE ON SCHEMA "1" TO dbapi_anonymous;
|
GRANT ALL ON SCHEMA "1" TO postgrest_test;
|
||||||
|
GRANT USAGE ON SCHEMA "1" TO postgrest_anonymous;
|
||||||
|
GRANT USAGE ON SCHEMA "1" TO postgrest_test_author;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2036 (class 0 OID 0)
|
-- TOC entry 2318 (class 0 OID 0)
|
||||||
-- Dependencies: 15
|
-- Dependencies: 19
|
||||||
|
-- Name: postgrest; Type: ACL; Schema: -; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
REVOKE ALL ON SCHEMA postgrest FROM PUBLIC;
|
||||||
|
REVOKE ALL ON SCHEMA postgrest FROM postgrest_test;
|
||||||
|
GRANT ALL ON SCHEMA postgrest TO postgrest_test;
|
||||||
|
GRANT USAGE ON SCHEMA postgrest TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2320 (class 0 OID 0)
|
||||||
|
-- Dependencies: 21
|
||||||
-- Name: public; Type: ACL; Schema: -; Owner: postgres
|
-- Name: public; Type: ACL; Schema: -; Owner: postgres
|
||||||
--
|
--
|
||||||
|
|
||||||
REVOKE ALL ON SCHEMA public FROM PUBLIC;
|
REVOKE ALL ON SCHEMA public FROM PUBLIC;
|
||||||
REVOKE ALL ON SCHEMA public FROM postgres;
|
|
||||||
GRANT ALL ON SCHEMA public TO postgres;
|
GRANT ALL ON SCHEMA public TO postgres;
|
||||||
GRANT ALL ON SCHEMA public TO PUBLIC;
|
GRANT ALL ON SCHEMA public TO PUBLIC;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2322 (class 0 OID 0)
|
||||||
|
-- Dependencies: 191
|
||||||
|
-- Name: authors_only; Type: ACL; Schema: 1; Owner: postgrest_test_author
|
||||||
--
|
--
|
||||||
|
|
||||||
REVOKE ALL ON TABLE authors_only FROM dbapi_test_author;
|
REVOKE ALL ON TABLE authors_only FROM PUBLIC;
|
||||||
GRANT ALL ON TABLE authors_only TO dbapi_test_author;
|
REVOKE ALL ON TABLE authors_only FROM postgrest_test_author;
|
||||||
|
GRANT ALL ON TABLE authors_only TO postgrest_test_author;
|
||||||
|
|
||||||
|
|
||||||
--
|
--
|
||||||
-- TOC entry 2290 (class 0 OID 0)
|
-- TOC entry 2323 (class 0 OID 0)
|
||||||
-- Dependencies: 175
|
-- Dependencies: 192
|
||||||
-- Name: auto_incrementing_pk; Type: ACL; Schema: 1; Owner: dbapi_test
|
-- Name: auto_incrementing_pk; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
REVOKE ALL ON TABLE auto_incrementing_pk FROM dbapi_test;
|
REVOKE ALL ON TABLE auto_incrementing_pk FROM PUBLIC;
|
||||||
GRANT ALL ON TABLE auto_incrementing_pk TO dbapi_test;
|
REVOKE ALL ON TABLE auto_incrementing_pk FROM postgrest_test;
|
||||||
GRANT ALL ON TABLE auto_incrementing_pk TO dbapi_anonymous;
|
GRANT ALL ON TABLE auto_incrementing_pk TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE auto_incrementing_pk TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
REVOKE ALL ON SEQUENCE auto_incrementing_pk_id_seq FROM dbapi_test;
|
|
||||||
GRANT ALL ON SEQUENCE auto_incrementing_pk_id_seq TO dbapi_test;
|
|
||||||
GRANT USAGE ON SEQUENCE auto_incrementing_pk_id_seq TO dbapi_anonymous;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
REVOKE ALL ON TABLE compound_pk FROM dbapi_test;
|
|
||||||
GRANT ALL ON TABLE compound_pk TO dbapi_test;
|
|
||||||
GRANT ALL ON TABLE compound_pk TO dbapi_anonymous;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
-- TOC entry 2325 (class 0 OID 0)
|
||||||
REVOKE ALL ON TABLE items FROM dbapi_test;
|
-- Dependencies: 193
|
||||||
GRANT ALL ON TABLE items TO dbapi_test;
|
-- Name: auto_incrementing_pk_id_seq; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
GRANT ALL ON TABLE items TO dbapi_anonymous;
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
REVOKE ALL ON SEQUENCE items_id_seq FROM dbapi_test;
|
|
||||||
GRANT ALL ON SEQUENCE items_id_seq TO dbapi_test;
|
|
||||||
GRANT USAGE ON SEQUENCE items_id_seq TO dbapi_anonymous;
|
|
||||||
|
|
||||||
--
|
--
|
||||||
|
|
||||||
REVOKE ALL ON TABLE menagerie FROM dbapi_test;
|
REVOKE ALL ON SEQUENCE auto_incrementing_pk_id_seq FROM PUBLIC;
|
||||||
GRANT ALL ON TABLE menagerie TO dbapi_test;
|
REVOKE ALL ON SEQUENCE auto_incrementing_pk_id_seq FROM postgrest_test;
|
||||||
GRANT ALL ON TABLE menagerie TO dbapi_anonymous;
|
GRANT ALL ON SEQUENCE auto_incrementing_pk_id_seq TO postgrest_test;
|
||||||
|
GRANT USAGE ON SEQUENCE auto_incrementing_pk_id_seq TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2326 (class 0 OID 0)
|
||||||
|
-- Dependencies: 194
|
||||||
|
-- Name: compound_pk; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
--
|
--
|
||||||
|
|
||||||
REVOKE ALL ON TABLE no_pk FROM dbapi_test;
|
REVOKE ALL ON TABLE compound_pk FROM PUBLIC;
|
||||||
GRANT ALL ON TABLE no_pk TO dbapi_test;
|
REVOKE ALL ON TABLE compound_pk FROM postgrest_test;
|
||||||
GRANT ALL ON TABLE no_pk TO dbapi_anonymous;
|
GRANT ALL ON TABLE compound_pk TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE compound_pk TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
REVOKE ALL ON TABLE has_fk FROM PUBLIC;
|
||||||
|
REVOKE ALL ON TABLE has_fk FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE has_fk TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE has_fk TO postgrest_anonymous;
|
||||||
|
|
||||||
REVOKE ALL ON TABLE simple_pk FROM dbapi_test;
|
--
|
||||||
GRANT ALL ON TABLE simple_pk TO dbapi_test;
|
-- TOC entry 2328 (class 0 OID 0)
|
||||||
GRANT ALL ON TABLE simple_pk TO dbapi_anonymous;
|
-- Dependencies: 197
|
||||||
|
-- Name: items; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
REVOKE ALL ON TABLE items FROM PUBLIC;
|
||||||
|
REVOKE ALL ON TABLE items FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE items TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE items TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2330 (class 0 OID 0)
|
||||||
|
-- Dependencies: 198
|
||||||
|
-- Name: items_id_seq; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
REVOKE ALL ON SEQUENCE items_id_seq FROM PUBLIC;
|
||||||
|
REVOKE ALL ON SEQUENCE items_id_seq FROM postgrest_test;
|
||||||
|
GRANT ALL ON SEQUENCE items_id_seq TO postgrest_test;
|
||||||
|
GRANT USAGE ON SEQUENCE items_id_seq TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2331 (class 0 OID 0)
|
||||||
|
-- Dependencies: 199
|
||||||
|
-- Name: menagerie; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
REVOKE ALL ON TABLE menagerie FROM PUBLIC;
|
||||||
|
REVOKE ALL ON TABLE menagerie FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE menagerie TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE menagerie TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2332 (class 0 OID 0)
|
||||||
|
-- Dependencies: 200
|
||||||
|
-- Name: no_pk; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
REVOKE ALL ON TABLE no_pk FROM PUBLIC;
|
||||||
|
REVOKE ALL ON TABLE no_pk FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE no_pk TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE no_pk TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2333 (class 0 OID 0)
|
||||||
|
-- Dependencies: 201
|
||||||
|
-- Name: simple_pk; Type: ACL; Schema: 1; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
REVOKE ALL ON TABLE simple_pk FROM PUBLIC;
|
||||||
|
REVOKE ALL ON TABLE simple_pk FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE simple_pk TO postgrest_test;
|
||||||
|
GRANT ALL ON TABLE simple_pk TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
|
SET search_path = postgrest, pg_catalog;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2334 (class 0 OID 0)
|
||||||
|
-- Dependencies: 202
|
||||||
|
-- Name: auth; Type: ACL; Schema: postgrest; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
|
REVOKE ALL ON TABLE auth FROM PUBLIC;
|
||||||
|
REVOKE ALL ON TABLE auth FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE auth TO postgrest_test;
|
||||||
|
GRANT INSERT ON TABLE auth TO postgrest_anonymous;
|
||||||
|
|
||||||
|
|
||||||
SET search_path = private, pg_catalog;
|
SET search_path = private, pg_catalog;
|
||||||
|
|
||||||
|
--
|
||||||
|
-- TOC entry 2335 (class 0 OID 0)
|
||||||
|
-- Dependencies: 203
|
||||||
|
-- Name: articles; Type: ACL; Schema: private; Owner: postgrest_test
|
||||||
|
--
|
||||||
|
|
||||||
REVOKE ALL ON TABLE articles FROM dbapi_test;
|
REVOKE ALL ON TABLE articles FROM PUBLIC;
|
||||||
GRANT ALL ON TABLE articles TO dbapi_test;
|
REVOKE ALL ON TABLE articles FROM postgrest_test;
|
||||||
|
GRANT ALL ON TABLE articles TO postgrest_test;
|
||||||
|
|
||||||
|
|
||||||
|
-- Completed on 2014-10-21 15:12:44 PDT
|
||||||
|
|
||||||
|
--
|
||||||
|
-- PostgreSQL database dump complete
|
||||||
--
|
--
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user