Compare commits

...
163 Commits
Author SHA1 Message Date
Joe Nelson 200e5a26cc Merge pull request #536 from begriffs/build-0.3.1.1
Bump version
2016-03-28 15:09:04 -07:00
Joe Nelson b8bbaa7764 Bump version 2016-03-28 13:26:06 -07:00
Joe Nelson 1470091f1c Merge pull request #534 from begriffs/unicode-schema
Regression test for read/write unicode table names
2016-03-27 00:04:30 -07:00
Joe Nelson 31738d745f Regression test for read/write unicode table names 2016-03-25 15:14:00 -07:00
Joe Nelson f19d4300bc Merge pull request #533 from begriffs/no-count-singular
Do not do table count when plurality=singular
2016-03-23 20:43:05 -07:00
Joe Nelson cd81e9346f Do not do table count when plurality=singular
Rebasing commits by @ruslantalpa
2016-03-23 20:30:51 -07:00
Joe Nelson 01355f39a1 Merge pull request #524 from begriffs/unicode-inserts
Preserve unicode in requests and responses
2016-03-18 11:59:36 -07:00
Joe Nelson 87298f580a Merge pull request #528 from rowdypixel/patch-1
Fix typo-d flag in the installation docs.
2016-03-18 09:47:46 -07:00
Joe Nelson 3bfe64dd06 Create monomorphic statement function to force use of Text 2016-03-16 21:04:18 -07:00
Dan Walker b9d3eedb9d Fix typo-d flag in the installation docs. 2016-03-14 21:28:46 -04:00
Joe Nelson bb4126bf3a Merge pull request #526 from daurnimator/no-uuid-ossp
Remove remaining uuid-ossp references
2016-03-14 09:18:45 -07:00
daurnimator 2e440822cb remove unnessecary create extension "uuid-ossp" 2016-03-14 20:52:51 +11:00
daurnimator 13eed84f57 Use gen_random_uuid instead of uuid_generate_v4 2016-03-14 20:51:35 +11:00
Joe Nelson e5fed86965 Changelog 2016-03-13 14:33:00 -07:00
Joe Nelson 3c5fab009b Remove ancient test comments 2016-03-13 14:22:36 -07:00
Joe Nelson b858626e17 For correctness include charset=utf-8 in responses 2016-03-13 14:22:17 -07:00
Joe Nelson 330cc91645 Protect unicode values in requests 2016-03-13 14:20:54 -07:00
Joe Nelson 1037824e11 Merge pull request #523 from begriffs/single-proc-call
Prevent duplicate call to stored procs
2016-03-12 23:34:25 -08:00
Joe Nelson 4cc08a11e7 Prevent duplicate call to stored procs
Reuse a CTE for results of call
2016-03-12 18:28:36 -08:00
Joe Nelson 358254639a Merge @ruslantalpa's fk improved detection 2016-03-12 12:42:57 -08:00
Joe Nelson 43bc9bfa83 Merge pull request #522 from begriffs/full-jwt
Allow SQL functions to generate registered JWT claims
2016-03-12 12:26:36 -08:00
Joe Nelson a779e9eb8b Batch the sql commands to set local vars 2016-03-11 23:45:05 -08:00
Joe Nelson f67e195f76 Expose all claims via sql postgrest.claims 2016-03-11 20:51:22 -08:00
Joe Nelson 508d722fb2 Allow SQL functions to generate registered JWT claims 2016-03-10 21:58:43 -08:00
Joe Nelson 14d7364f4b Merge pull request #521 from dex-ethics/spelling
Spelling fixes in documentation
2016-03-09 12:15:28 -08:00
Remco Bloemen bfbce27a65 Spelling fixes in documentation 2016-03-09 15:47:12 +01:00
Joe Nelson 00a23058c8 Merge pull request #511 from dex-ethics/docker-exec
Use `CMD exec` in Dockerfile
2016-03-07 22:34:16 -08:00
Remco Bloemen 82c74ed21f Use CMD exec in Dockerfile
Without exec the `postgrest` process is not run with PID 1 (it
is a child process of the shell that starts it). This means
signals send to the docker (like `docker stop` or ^C) will
not be handled correctly.

However, Linux treats PID 1 as special and sets the SIGTERM
handler to ignore by default. It is also necessary to install
a SIGTERM handler.

This commit adds `exec` to resolve this problem, as per the
recommendation in the Dockerfile documentation:

https://docs.docker.com/engine/reference/builder/#shell-form-entrypoint-example
2016-03-07 18:30:40 +01:00
Joe Nelson 5f0b4977da Merge pull request #514 from dex-ethics/docs
Minor changes in documentation
2016-03-07 09:15:16 -08:00
Remco Bloemen 82214856b6 Split build and install in build from source instructions.
Stack refuses to build when run under sudo.
2016-03-07 17:54:37 +01:00
Remco Bloemen c09adb967a Use gen_random_uuid() in user management example.
The function uuid_generate_v4() is not available
without extensions.
2016-03-07 17:53:55 +01:00
Remco Bloemen e5d420b2db Gracefull exit on sigTERM
Like the sigINT that was already handled, postgrest
should gracefuly shut down on a sigTERM. This is a
common way of stopping processes, used amongst
others by docker.

See: https://stackoverflow.com/questions/4042201/how-does-sigint-relate-to-the-other-termination-signals
2016-03-07 17:45:53 +01:00
Joe Nelson ef021056c9 Merge pull request #497 from bobcolner/bobcolner-dockerfile
PostgREST Dockerfile
2016-03-05 13:20:05 -08:00
Bob Colner b7b082cd8e updated Dockerfile to use postgrest 3.1.0 2016-03-05 13:07:11 -08:00
Bob Colner a02632f18c Update Dockerfile 2016-03-05 12:53:33 -08:00
Ruslan Talpa e43ad54dbf Merge branch 'master' of https://github.com/begriffs/postgrest 2016-03-01 17:54:55 +02:00
Joe Nelson 8af91e262c Merge pull request #508 from begriffs/test-plain-build
Test that binary builds, not just that suite passes
2016-02-29 22:49:33 -08:00
Ruslan Talpa 7b94fb608d suggestions by @diogob 2016-03-01 08:08:57 +02:00
Joe Nelson cf176c4100 Allow aeson v11, but forbid deadly v10 2016-02-29 21:03:42 -08:00
Joe Nelson c61418635e Ensure helper binaries get re-installed
Sadly causes all extra-deps to rebuild every time
2016-02-29 20:45:34 -08:00
Joe Nelson dba827d1fd List missing other-module in spec 2016-02-29 14:52:01 -08:00
Joe Nelson e315ad99b4 Name the main module "Main" as required 2016-02-29 14:50:44 -08:00
Joe Nelson 088df7e6be Test that binary build succeeds
Work around https://github.com/commercialhaskell/stack/issues/1846
2016-02-29 14:25:34 -08:00
Ruslan Talpa 40eec0b2ff code beautify using stylish-haskell 2016-02-29 14:53:41 +02:00
Ruslan Talpa 77bec52be7 Fix compile notice 2016-02-29 14:11:48 +02:00
Ruslan Talpa 155d1dee6b changelog entry 2016-02-29 13:59:04 +02:00
Ruslan Talpa 0548d65911 main module of the executable needs to be Main, with PostgREST.Main build fails 2016-02-29 13:57:34 +02:00
Ruslan Talpa 40a30d7b02 Fix view column source detection 2016-02-29 13:10:33 +02:00
Ruslan Talpa 62af792add Add failing test to test correct view column detection 2016-02-29 11:37:23 +02:00
Joe Nelson 4cd2475bf2 v0.3.1.0 2016-02-28 21:45:17 -08:00
Joe Nelson fc4c792f9e Move section in changelog 2016-02-26 12:17:24 -08:00
Joe Nelson c094e5a0fc Merge pull request #489 from diogob/apply_range_headers_to_rpc
Apply range headers to rpc
2016-02-26 12:10:33 -08:00
Diogo Biazus 9d0f3573c6 Implements query counting in proc call and adds Content-Rage to response
headers in /rpc calls.
2016-02-26 14:51:35 -05:00
Diogo Biazus 4496a95014 Updates changelog 2016-02-26 14:41:27 -05:00
Diogo Biazus 893b7a7126 Applies range headers to /rpc calls using LIMIT/OFFSET. 2016-02-26 14:41:27 -05:00
Joe Nelson 3b23c4aa5b Merge pull request #503 from begriffs/one-tx-per-client
Reduces pool resource locking (2)
2016-02-26 10:11:47 -08:00
Joe Nelson d466ea45ff Add changelog entry
Nice work guys, this took a lot of cooperation
2016-02-26 10:06:54 -08:00
Joe Nelson 7ba5363d25 Upgrade hasql to fix prepared statement problem 2016-02-26 08:18:47 -08:00
Joe Nelson f28b03f419 Allow new hasql-transaction to do rollbacks 2016-02-25 20:13:03 -08:00
Joe Nelson de772b9246 Modified the concurrent test to illustrate problem with prepared statement 2016-02-22 20:58:44 -08:00
Joe Nelson c28b26d949 Run QueryLimitedSpec with its own server flags 2016-02-22 19:21:39 -08:00
Joe Nelson c02dd4aa98 Enable real threads in test 2016-02-22 17:53:10 -08:00
Joe Nelson b0974a4e36 Reset db between each test suite 2016-02-22 17:50:00 -08:00
Joe Nelson 17acd134c7 Suppress server logging in test mode 2016-02-22 17:48:33 -08:00
Joe Nelson d4a4bbf966 Roll back on db errors 2016-02-22 16:52:50 -08:00
Joe Nelson 7b7babd1d1 Fix frozen tests
Problem found by @ruslantalpa
2016-02-22 08:41:31 -08:00
Joe Nelson 072a6ce4c7 Bump hasql to 0.19.8 2016-02-21 18:37:17 -08:00
Joe Nelson d5c1438c6e Use hasql-transaction
Also use hspec before-wrapper
2016-02-21 18:05:25 -08:00
Joe Nelson 30e5032ade Use lower optimization to speed up regular dev builds 2016-02-21 14:11:46 -08:00
Joe Nelson d7fe59f0b0 WIP: share server code between tests and program
- Share server code in Main
- Switch to hasql-pool
- Use pool in tests
- DRY up test runner
2016-02-21 12:22:18 -08:00
Joe Nelson 8a006f07a7 Show error text more clearly 2016-02-20 18:11:46 -08:00
Diogo BiazusandJoe Nelson 01ab540ffe Simplify return from withResource in Main.hs 2016-02-20 18:03:17 -08:00
Diogo BiazusandJoe Nelson de848f64fa Return the results from withResource function before applying the respond continuation. This ensures that the pool resource is freed as soon as the database operation is complete 2016-02-20 18:03:06 -08:00
Joe Nelson 52e689b830 Add concurrent test for "transaction in progress"
MonadBaseControl wizardry courtesy of @jwiegley
2016-02-20 17:45:55 -08:00
Bob Colner ef3e2511fe PostgRest Dockerfile
PostgRest Dockerfile with ENV parameter passthrough.
2016-02-17 10:41:34 -08:00
Joe Nelson 6b4b763bc4 Merge pull request #494 from begriffs/test-raw-cabal
Ensure plain cabal can determine a build plan
2016-02-15 11:46:31 -08:00
Joe Nelson 6b1c8b3e39 Ensure plain cabal can determine a build plan
For those wishing to use postgrest as a library
2016-02-14 22:17:26 -08:00
Joe Nelson f3293cfac1 Do not name import of void directly as it is used conditionally 2016-02-12 23:15:09 -08:00
Joe Nelson 0dd8a498b2 v0.3.0.4 2016-02-12 20:45:03 -08:00
Joe Nelson f9b8e6879d Merge pull request #492 from begriffs/threaded
Do not block threads on libpq ffi
2016-02-12 20:32:40 -08:00
Joe Nelson e73a4c66bc Do not block threads on libpq ffi
Fixes #445
2016-02-12 14:43:00 -08:00
Joe Nelson fc3c885bb6 Merge pull request #491 from begriffs/payload-error
Ensure payload parse errors are not suppressed
2016-02-11 16:39:13 -08:00
Joe Nelson 8b3d224b80 Ensure payload parse errors are not suppressed
Fixes #490
2016-02-11 16:24:02 -08:00
Joe Nelson ce6e52e9ba Merge pull request #487 from motiz88/nitpick
Minor proofreading in InsertSpec
2016-02-08 08:23:22 -08:00
Moti Zilberman 4dd4eeb421 [ci skip] s/unkonwn/unknown/ 2016-02-08 16:39:32 +02:00
Moti Zilberman b50882db3a [ci skip] Fix typo in InsertSpec 2016-02-08 15:53:15 +02:00
Moti Zilberman 0058b5df99 Minor proofreading in InsertSpec
s/csv/json/ in line 24
2016-02-08 14:29:47 +02:00
Joe Nelson f7926e9f28 Merge pull request #486 from begriffs/improved-usage
Adjust usage message to indicate required arguments
2016-02-06 18:02:09 -08:00
Joe Nelson f65557573c Match new config param order in tests 2016-02-05 09:39:43 -08:00
Joe Nelson 4dec445b82 Add example db connection string to usage 2016-02-05 09:30:42 -08:00
Joe Nelson ccb3eba9e3 Reorder arg list, note that schema is not required 2016-02-05 09:27:24 -08:00
Joe Nelson 56426b896a Changelog entry 2016-02-04 16:38:35 -08:00
Joe Nelson 7702d38267 Update docs to note rquired arguments 2016-02-04 16:34:36 -08:00
Joe Nelson a044398552 Adjust usage message to indicate required arguments 2016-02-04 16:27:54 -08:00
Joe Nelson 17db68ae2d Merge branch 'feature/options-not-found-error' 2016-02-03 11:44:36 -08:00
Joe Nelson e53fb10483 Fix lint 2016-02-03 11:36:17 -08:00
calebmerandJoe Nelson 33757e537b Fix options on non existant tables, closes #442 2016-02-03 11:20:45 -08:00
Joe Nelson 945ef61188 Merge pull request #483 from begriffs/rpc-post-only
Issue http 405 for anything but POST on RPC
2016-02-03 11:16:51 -08:00
Joe Nelson f990a519a5 More extensive tests 2016-02-02 16:45:54 -08:00
Joe Nelson 2149bea8e3 Merge pull request #485 from motiz88/patch-1
Document default order direction correctly, fix #484
2016-02-02 15:47:14 -08:00
Moti Zilberman 7ce10dbcf1 Correct default order direction, fix #484 2016-02-03 01:36:54 +02:00
Joe Nelson 31f46d5220 Direct stack download is faster than apt-get 2016-02-01 23:40:02 -08:00
Joe Nelson a0ef4eae4d Issue http 405 for anything but POST on RPC
Fixes #481
2016-02-01 22:52:57 -08:00
Joe Nelson aec11e34a7 Merge pull request #480 from eGullGolf/issue169
Explain how to connect without a password through Unix socket
2016-02-01 12:55:42 -08:00
Eric Bréchemier 95f26604ba Explain how to connect without a password through Unix socket
I expanded the section 'Running the Server' in the documentation.

Based on this comment:
https://github.com/begriffs/postgrest/issues/169#issuecomment-177031035

Related issue: #169
2016-02-01 11:16:26 +01:00
Joe Nelson c6d47eeb77 Merge pull request #478 from pra85/patch-1
Typo in Readme.md
2016-01-30 20:07:06 -08:00
Prayag Verma 8500067e0f Typo in Readme.md
commicate > communicate
2016-01-31 09:18:39 +05:30
Joe Nelson 20573632d7 Merge pull request #476 from begriffs/stack-ci
Switch to Stack on CircleCI
2016-01-29 14:34:27 -08:00
Joe Nelson 03468c83df Merge pull request #477 from sscarduzio/patch-3
#475
2016-01-29 10:10:49 -08:00
Simone Scarduzio 4e3a04ea72 #475 2016-01-29 18:06:29 +00:00
Joe Nelson e6e324e8ff Trim unused packages 2016-01-28 17:32:38 -08:00
Joe Nelson 2175ae4d28 Build test deps in cacheable phase 2016-01-28 16:36:45 -08:00
Joe Nelson c887f2b3b4 Move ghc-options in stack config and out of cabal file 2016-01-28 14:03:44 -08:00
Joe Nelson 1a3c54793d Build only haddocks for postgrest, not deps 2016-01-28 14:00:33 -08:00
Joe Nelson 4679e2a514 Haddock coverage report not working, removing for now 2016-01-28 13:17:49 -08:00
Joe Nelson e02dc2e92e Remove redundant do 2016-01-27 18:51:53 -08:00
Joe Nelson 536acec820 Turn down warning level a little 2016-01-27 17:31:57 -08:00
Joe Nelson 55da918240 Build with stack on CI and use harsher setttings 2016-01-27 16:12:48 -08:00
Joe Nelson f3d4d1fb60 Merge pull request #472 from begriffs/hasql-19
Upgrade to Hasql 19
2016-01-27 14:59:08 -08:00
Joe Nelson dac31c4f2e Use newer LTS to avoid potential aeson problem 2016-01-27 11:00:40 -08:00
Joe Nelson 677c73cfe5 New versions of Warp do not export Connection 2016-01-24 18:09:21 -08:00
Joe Nelson b85fc37130 Protect against nulls that choke our decoder 2016-01-24 18:09:20 -08:00
Joe Nelson f634b7fe98 Rollback test connection on errors 2016-01-24 18:09:20 -08:00
Joe Nelson 75ebd1bd24 Derp, it is "commit" not "end" 2016-01-24 18:09:20 -08:00
Joe Nelson cbb2ba7d42 Appease hlint 2016-01-24 18:09:20 -08:00
Joe Nelson 616541aaee Use reorder-goals in CI to make vanilla cabal work 2016-01-24 18:09:20 -08:00
Joe Nelson bbf8365cd2 Upgrade hasql 2016-01-24 18:09:20 -08:00
Joe Nelson a0b390e735 Avoid possibilities of null for our chosen decoder 2016-01-24 18:09:20 -08:00
Joe Nelson 1f557a92a4 Pass query args properly 2016-01-24 18:09:20 -08:00
Joe Nelson 51f71eb53d Run queries in a transaction again 2016-01-24 18:09:19 -08:00
Joe Nelson ac73e8d77b Remove connection pooling in test 2016-01-24 18:09:19 -08:00
Joe Nelson 8b13e7dd73 Header cannot be null even when it is n/a 2016-01-24 18:09:19 -08:00
Joe Nelson 3be04d7f30 Upgrade hasql to fix connection error 2016-01-24 18:09:19 -08:00
Joe Nelson b9fd083c77 It all compiles but all requests give a postgres error 2016-01-24 18:09:19 -08:00
Joe Nelson fec316b087 WIP: fixing compiler errors in specs 2016-01-24 18:09:19 -08:00
Joe Nelson 3844f3ee96 The app compiles but totally untested 2016-01-24 18:09:19 -08:00
Joe Nelson cb3977679d App.hs compiles (dubiously)
Removed query body is no longer a maybe value
2016-01-24 18:09:19 -08:00
Joe Nelson 6122bc4108 Middleware compiles 2016-01-24 18:09:18 -08:00
Joe Nelson abc30d5170 DbStructure compiles 2016-01-24 18:09:18 -08:00
Joe Nelson 4b515c5df4 WIP: converting DbStructure 2016-01-24 18:09:18 -08:00
Joe Nelson 2d5210464a WIP: converting dbstructure 2016-01-24 18:09:18 -08:00
Joe Nelson 684b11badb WIP: converting DbStructure 2016-01-24 18:09:18 -08:00
Joe Nelson 7b92449343 QueryBuilder compiles with hasql 19 2016-01-24 18:09:18 -08:00
Joe Nelson 72cd6c37bd Change some import statements and Session types 2016-01-24 18:09:18 -08:00
Joe Nelson d6102cc908 Use the correct Error types for hasql 0.19 2016-01-24 18:09:18 -08:00
Joe Nelson 5faa80b172 Merge pull request #470 from sscarduzio/patch-1
The correct HTTP status code for integrity constraint violation is 509, fixes #469
2016-01-24 15:31:55 -08:00
Simone Scarduzio 74d76c690f Adding two test cases for 469 2016-01-24 14:52:14 +00:00
Joe Nelson 301d9b6a86 Merge pull request #468 from guido4000/patch-1
Rename endpoint for consistent endpoint names
2016-01-21 08:25:59 -08:00
Simone Scarduzio b5e6a93b32 Being more specific 2016-01-21 15:21:30 +00:00
Simone Scarduzio e7c711002a The correct HTTP status code for integrity constraint violation is 509, fixes #469 2016-01-21 14:37:47 +00:00
Guido Hoermann 6dce40e454 Rename endpoint for consistent endpoint names
In line 126 the endpoint url is api_token. Therefore it should be the same in line 178.
2016-01-21 20:26:25 +07:00
Joe Nelson 9ba603660e Merge pull request #466 from diogob/adds_multiple_insert_section
Adds section about multiple insertion/update to api/writing docs [#405]
2016-01-20 21:13:29 -08:00
Diogo Biazus 5ee44c6c21 Adds section about multiple insertion/update to api/writing docs [#405] 2016-01-20 23:10:24 -05:00
Joe Nelson 03bec64097 Merge pull request #450 from daurnimator/patch-1
docs/examples/users.md: pgcrypto includes uuidv4 generation
2016-01-20 18:28:03 -08:00
Joe Nelson 4fcc0fbc94 Merge pull request #464 from eGullGolf/issue463
Add step to install libpq-dev when building from source on Ubuntu
2016-01-19 11:25:26 -08:00
Eric Bréchemier 80ade96e9b Add step to install libpq-dev when building from source on Ubuntu
This is a dependency of postgresql-libpq-0.9.1.1, which results in
a failure when missing during stack build.

Closes #463.
2016-01-19 20:14:29 +01:00
Joe Nelson 860e437078 Merge pull request #459 from motiz88/tentative-fix-458
pgFmtLit: Change condition for using E"..." syntax
2016-01-15 09:14:35 -08:00
Moti Zilberman 9a596c2500 pgFmtLit: Change condition for using E"..." syntax, fix #458.
Includes a test spec by @diogob.
2016-01-15 11:59:25 +02:00
Joe Nelson 0d9d74dc1c Merge pull request #454 from ruslantalpa/fix_453
fix #453 by sending the request to the db (instead of using the cached data)
2016-01-13 20:29:05 -08:00
Ruslan Talpa 88d98d6d62 fix #453 by sending the request to the db (instead of using the cached data) 2016-01-11 10:17:34 +02:00
Joe Nelson c93c4d8c30 Merge pull request #452 from manufaktor/patch-1
Set login permission for authenticator role
2016-01-09 10:15:35 -08:00
Immanuel Häussermann 021e78d962 Set login permission for authenticator role
This was confusing when I got started with the docs. When using CREATE ROLE, the role will not have the LOGIN permission and starting the server with `postgrest postgres://authenticator@localhost:5432/mydb --anonymous anon` will result in `FATAL: role "authenticator" is not permitted to log in`. The docs should either use CREATE USER or set LOGIN when using CREATE ROLE, as I understand it.
2016-01-09 15:47:38 +01:00
daurnimator 96533fa2fe docs/examples/users.md: pgcrypto includes uuidv4 generation
No need to use the uuid-ossp extension
2016-01-08 13:20:57 +11:00
41 changed files with 1726 additions and 1010 deletions
+30
View File
@@ -3,6 +3,36 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
### Fixed
## [0.3.1.1] - 2016-03-28
### Fixed
- Preserve unicode values in insert,update,rpc (regression) - @begriffs
- Prevent duplicate call to stored procs (regression) - @begriffs
- Allow SQL functions to generate registered JWT claims - @begriffs
- Terminate gracefully on SIGTERM (for use in Docker) - @recmo
- Relation detection fix for views that depend on multiple tables - @ruslantalpa
- Avoid count on plurality=singular and allow multiple Prefer values - @ruslantalpa
## [0.3.1.0] - 2016-02-28
### Fixed
- Prevent query error from infecting later connection - @begriffs, @ruslantalpa, @nikita-volkov, @jwiegley
### Added
- Applies range headers to RPC calls - @diogob
## [0.3.0.4] - 2016-02-12
### Fixed
- Improved usage screen - @begriffs
- Reject non-POSTs to rpc endpoints - @begriffs
- Throw an error for OPTIONS on nonexistent tables - @calebmer
- Remove deadlock on simultaneous contentious updates - @ruslantalpa, @begriffs
## [0.3.0.3] - 2016-01-08
### Fixed
+27
View File
@@ -0,0 +1,27 @@
FROM debian:jessie
ENV POSTGREST_VERSION 0.3.1.0
ENV POSTGREST_SCHEMA public
ENV POSTGREST_ANONYMOUS postgres
ENV POSTGREST_JWT_SECRET thisisnotarealsecret
ENV POSTGREST_MAX_ROWS 1000000
ENV POSTGREST_POOL 200
RUN apt-get update && \
apt-get install -y tar xz-utils wget libpq-dev && \
apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
RUN wget http://github.com/begriffs/postgrest/releases/download/v${POSTGREST_VERSION}/postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
tar --xz -xvf postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
mv postgrest /usr/local/bin/postgrest && \
rm postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz
CMD exec postgrest postgres://${PG_ENV_POSTGRES_USER}:${PG_ENV_POSTGRES_PASSWORD}@${PG_PORT_5432_TCP_ADDR}:${PG_PORT_5432_TCP_PORT}/${PG_ENV_POSTGRES_DB} \
--port 3000 \
--schema ${POSTGREST_SCHEMA} \
--anonymous ${POSTGREST_ANONYMOUS} \
--pool ${POSTGREST_POOL} \
--jwt-secret ${POSTGREST_JWT_SECRET} \
--max-rows ${POSTGREST_MAX_ROWS}
EXPOSE 3000
+1 -1
View File
@@ -125,7 +125,7 @@ respond to the OPTIONS verb and explain what they support as well
as the data format of their JSON payload. RAML support is an upcoming
feature.
The project uses HTTP itself to commicate other metadata. For
The project uses HTTP itself to communicate other metadata. For
instance 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).
+1 -1
View File
@@ -10,7 +10,7 @@
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.3.0.3"
"value": "0.3.1.1"
},
"DB_NAME": {
"description": "Database name",
+21 -11
View File
@@ -1,16 +1,26 @@
machine:
dependencies:
cache_directories:
- "~/.stack"
- ".stack-work"
pre:
- curl -L https://github.com/commercialhaskell/stack/releases/download/v1.0.2/stack-1.0.2-linux-x86_64.tar.gz | tar zx -C /tmp
- sudo mv /tmp/stack-1.0.2-linux-x86_64/stack /usr/bin
- createuser --superuser --no-password postgrest_test
- createdb -O postgrest_test -U ubuntu postgrest_test
ghc:
version: 7.10.1
dependencies:
override:
- cabal update
- cabal sandbox init
- cabal install --upgrade-dependencies --constraint="template-haskell installed" --dependencies-only --enable-tests
- cabal configure --enable-tests -f ci
- stack setup
- rm -fr $(stack path --dist-dir) $(stack path --local-install-root)
- stack install hlint packdeps cabal-install
- stack build
- stack build --test --no-run-tests
test:
post:
- cabal exec hlint -- -X QuasiQuotes src/**/*.hs test/**/*.hs
- cabal exec packdeps postgrest.cabal || true
override:
- stack test
- git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes "$@"
- stack exec -- cabal update
- stack exec --no-ghc-package-path -- cabal install --only-d --dry-run
- stack exec -- packdeps *.cabal || true
- stack exec -- cabal check
- stack haddock --no-haddock-deps
- stack sdist
+1 -1
View File
@@ -60,7 +60,7 @@ for anonymous users, one for authors, and another for the authenticator,
you would set it up like this
```sql
CREATE ROLE authenticator NOINHERIT;
CREATE ROLE authenticator NOINHERIT LOGIN;
CREATE ROLE anon;
CREATE ROLE author;
+6 -1
View File
@@ -159,7 +159,7 @@ comma-separated list of columns and directions:
GET /people?order=age.desc,height.asc
```
If no direction is specified it defaults to descending order:
If no direction is specified it defaults to ascending order:
```HTTP
GET /people?order=age
@@ -285,6 +285,11 @@ GET /projects?id=eq.1&select=id, name, client{*}
Would embed in the `client` key the row referenced with `client_id`.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>In order for this feature to work as expected after a schema change, PostgREST currently requires to be restarted.</p>
</div>
### Response Format
Query responses default to JSON but you can get them in CSV as well. Just make your request with the header
+88 -1
View File
@@ -71,6 +71,93 @@ returns something like
[ { "id": 1 }, { "id": 2 } ]
```
### Multiple Tables Insertion or Update
The cleanest way to insert or update data into multiple tables using only one POST/PATCH request
is to create a view that will join all target tables and present a single endpoint.
In our example let's assume one users table and one companies table.
In this case, we want a signup endpoint to create the first user within a company.
And for this endpoint we want to insert with one request both user and company.
```SQL
CREATE TABLE companies (
id serial primary key,
name text unique
);
CREATE TABLE users (
id serial primary key,
name text not null,
pass text,
company_id integer not null references companies
);
```
Having both tables created we create a view that joins them to be used
as a ```/signup``` endpoint.
```SQL
CREATE VIEW signup AS
SELECT
c.name AS company_name,
u.name AS user_name,
u.pass
FROM
public.users u
JOIN public.companies c ON c.id = u.company_id;
```
After the signup view creation, we can issue ```GET``` requests to read data
from users and companies, but any atempt to ```POST``` or ```PATCH``` data will fail.
PostgreSQL won't allow any data change on views that have a ```JOIN```
clause in their ```FROM``` without a proper ```INSTEAD OF``` trigger.
So in the example bellow we create a trigger to allow insertion of data in the signup view.
The trigger is a simple PL/pgSQL function that first inserts into the companies table and
uses the newly create company_id to create its first user.
```SQL
CREATE FUNCTION signup()
RETURNS trigger
LANGUAGE plpgsql
AS $$
DECLARE
vcompany_id int;
BEGIN
INSERT INTO companies (name) VALUES (new.company_name) RETURNING id INTO vcompany_id;
INSERT INTO users (name, pass, company_id) VALUES (new.user_name, new.pass, vcompany_id);
RETURN new;
END;
$$;
CREATE TRIGGER signup
INSTEAD OF INSERT ON signup
FOR EACH ROW
EXECUTE PROCEDURE signup();
```
After the trigger creation we can issue a normal ```POST``` request to our signup endpoint:
```HTTP
POST /signup
{ "company_name": "foo", "user_name": "bar" }
```
For an endpoint such as signup its usually not desirable to have a ```PATCH``` route for updates,
and we will skip this example for the sake of brevity. But it would be implemented in a very
similar way to our ```POST``` example.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>It's advisable to create a separate trigger for <code>UPDATE</code> and <code>INSERT</code>
avoiding conditionals that decide which is the trigger current operation.
This makes it easier to change code for (or even disable) one operation without interfering with others while
improving readability.
</p>
</div>
### Bulk Updates
* ❌ Cannot be cached or prefetched
@@ -99,7 +186,7 @@ basic field replacements, and not at all "incorrect."
* ❌ Cannot be cached or prefetched
* ✅ Idempotent
Simply use the `DELETE` verb. All recors that match your filter
Simply use the `DELETE` verb. All records that match your filter
will be removed. For instance deleting inactive users:
```HTTP
+2 -2
View File
@@ -43,7 +43,7 @@ ALTER TABLE users ADD role text NOT NULL DEFAULT 'customer';
```
Besides the main user that PostgREST uses to connect to PostgreSQL
and the anonymous user, we will need two aditional roles for our example:
and the anonymous user, we will need two additional roles for our example:
* admin - to be used by users that access all the system rows.
* customer - to be used when user has restricted access to database rows.
@@ -175,7 +175,7 @@ WHERE
Now whenever you are authenticated in your Rails application you can use some Javascript
code to get the token and use it:
```javascript
$.getJSON('/api_json').done(function(data){
$.getJSON('/api_token').done(function(data){
$.ajax('/orders', {'Authorization': 'Bearer ' + data.token}).done(function(data){
console.log('Visible Orders: ', data);
})
+8 -10
View File
@@ -8,12 +8,12 @@ a username and password system on top of JWT using only plpgsql.
Future examples such as the multi-tenant blogging platform will use
the results from this example for their auth. We will build a system
for users to sign up, log in, manage their accounts, and for admins
to manange other people's accounts. We will also see how to trigger
to manage other people's accounts. We will also see how to trigger
outside events like sending password reset emails.
Before jumping into the code, a little more about how the tokens
work. Every JWT contains cryptographically signed *claims*. PostgREST
cares specificaly about a claim called `role`. When a client includes
cares specifically about a claim called `role`. When a client includes
a `role` claim PostgREST executes their request using that database
role.
@@ -28,13 +28,11 @@ value.
### Storing Users and Passwords
We create a database schema especially for auth information. We'll
also need the postgres extensions
[pgcrypto](http://www.postgresql.org/docs/current/static/pgcrypto.html) and
[uuid-ossp](http://www.postgresql.org/docs/current/static/uuid-ossp.html).
also need the postgres extension
[pgcrypto](http://www.postgresql.org/docs/current/static/pgcrypto.html).
```sql
create extension if not exists pgcrypto;
create extension if not exists "uuid-ossp";
-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
@@ -150,7 +148,7 @@ begin
where token_type = 'reset'
and tokens.email = request_password_reset.email;
select uuid_generate_v4() into tok;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', request_password_reset.email);
perform pg_notify('reset',
@@ -226,7 +224,7 @@ begin
where token_type = 'reset'
and tokens.email = reset_password.email;
select uuid_generate_v4() into tok;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', reset_password.email);
perform pg_notify('reset',
@@ -253,7 +251,7 @@ basic_auth.send_validation() returns trigger
declare
tok uuid;
begin
select uuid_generate_v4() into tok;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'validation', new.email);
perform pg_notify('validate',
@@ -296,7 +294,7 @@ where actual.role = member_of.rolname;
-- is equal to email so that user can only see themselves
```
Using this view clients can see themeslves and any other users with
Using this view clients can see themselves and any other users with
the right db roles. This view does not yet support inserts or updates
because not all the columns refer directly to underlying columns.
Nor do we want it to be auto-updatable because it would allow an escalation
+41 -5
View File
@@ -46,12 +46,17 @@ wget -q -O- https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key | s
echo 'deb http://download.fpcomplete.com/ubuntu/trusty stable main'|sudo tee /etc/apt/sources.list.d/fpco.list
sudo apt-get update && sudo apt-get install stack -y
```
* Install libpq-dev
```
sudo apt-get install -y libpq-dev
```
* Build & install in one step
```bash
git clone https://github.com/begriffs/postgrest.git
cd postgrest
sudo stack install --install-ghc --local-bin-path /usr/local/bin
stack build --install-ghc
sudo stack install --allow-different-user --local-bin-path /usr/local/bin
```
* Run the server
@@ -61,7 +66,7 @@ If you want to run the test suite, stack can do that too: `stack test`.
### Running the Server
```bash
postgrest postgres://user:pass@host:port/db [flags]
postgrest postgres://user:pass@host:port/db -a anon_user [other flags]
```
The user in the connection string is the "authenticator role," i.e.
@@ -69,14 +74,14 @@ a role which is used temporarily to switch into other roles depending
on the authentication request JWT. For simple API's you can use the
same role for authenticator and anonymous.
The possible flags are:
The complete list of options:
<dl>
<dt>-p, --port</dt>
<dd>The port on which the server will listen for HTTP requests.
Defaults to 3000.</dd>
<dt>-a, --anonymous</dt>
<dt>-a, --anonymous (required)</dt>
<dd>The database role used to execute commands for those requests
which provide no JWT authorization.</dd>
@@ -90,7 +95,7 @@ The possible flags are:
<code>secret</code> but do not use the default in production!
Load-balanced PostgREST servers should share the same secret.</dd>
<dt>-p, --pool</dt>
<dt>-o, --pool</dt>
<dd>Max connections to use in db pool. Defaults to to 10, but you
should find an optimal value for your db by running the SQL
command <code>show max_connections;</code></dd>
@@ -113,6 +118,37 @@ The possible flags are:
file.</p>
</div>
When running `postgrest` on the same machine as PostgreSQL, it is also
possible to connect to the database using the [Unix socket]
(https://en.wikipedia.org/wiki/Unix_domain_socket) and the
[Peer Authentication method]
(http://www.postgresql.org/docs/current/static/auth-methods.html#AUTH-PEER)
as an alternative to TCP/IP communication and authentication with a password.
The Peer Authentication grants access to the database to any Unix user
who connects as a user of the same name in the database.
Since the empty host resolves to the Unix socket]
(http://www.postgresql.org/docs/current/static/libpq-connect.html#AEN42494)
and the password can be omitted in this case,
the command line is reduced to:
```sh
sudo -u user postgrest postgres://user@/db [flags]
```
where the `sudo -u user` command runs the following command as given `user`.
If you create a Unix user `postgrest` and a database user `postgrest`
for example, the command becomes:
```sh
sudo -u postgrest postgrest postgres://postgrest@/db [flags]
```
The first `postgrest` is the Unix user name, the second `postgrest`
is the name of the executable, the third `postgrest` is the name
of the database user.
### Install via Homebrew (Mac OS X)
You can use the Homebrew package manager to install PostgREST on Mac
+43 -35
View File
@@ -2,7 +2,7 @@ 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.3.0.3
version: 0.3.1.1
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
@@ -22,25 +22,27 @@ Flag CI
Default: False
executable postgrest
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
main-is: PostgREST/Main.hs
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
ghc-options: -threaded -rtsopts -with-rtsopts=-N
default-language: Haskell2010
build-depends: aeson >= 0.8
build-depends: aeson (>= 0.8 && < 0.10) || (>= 0.11 && < 0.12)
, base >= 4.8 && < 5
, bytestring
, case-insensitive
, cassava
, containers
, contravariant
, errors
, hasql >= 0.7.3 && < 0.8
, hasql-backend >= 0.4.1 && < 0.5
, hasql-postgres >= 0.10.4 && < 0.11
, hasql >= 0.19.9 && < 0.20
, hasql-pool >= 0.4 && < 0.5
, hasql-transaction >= 0.4.3 && < 0.5
, http-types
, interpolatedstring-perl6
, jwt
, lens >=3.8 && < 5.0
, lens-aeson >= 1.0.0.0 && < 1.1.0.0
, mtl
, optparse-applicative >= 0.11 && < 0.13
, parsec
, postgrest
@@ -50,16 +52,14 @@ executable postgrest
, string-conversions
, text
, time
, transformers
, unordered-containers
, vector
, wai >= 3.0.1
, wai-cors
, wai-extra
, wai-middleware-static >= 0.6.0
, warp >= 3.0.2
, HTTP, http-types
, MissingH
, warp >= 3.1.0
, HTTP
, Ranged-sets
if !os(windows)
build-depends: unix >= 2.7 && < 3
@@ -79,11 +79,6 @@ executable postgrest
, PostgREST.Types
library
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
default-language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
build-depends: aeson
@@ -92,12 +87,17 @@ library
, case-insensitive
, cassava
, containers
, contravariant
, errors
, hasql
, hasql-backend
, hasql-postgres
, hasql-transaction
, hasql-pool
, http-types
, interpolatedstring-perl6
, jwt
, lens
, lens-aeson
, mtl
, optparse-applicative
, parsec
, regex-tdfa
@@ -108,13 +108,13 @@ library
, time
, unordered-containers
, vector
, wai
, HTTP
, Ranged-sets
, wai >= 3.0.1
, wai-cors
, wai-extra
, wai-middleware-static
, HTTP
, MissingH
, Ranged-sets
, wai-middleware-static >= 0.6.0
, warp >= 3.1.0
Other-Modules: Paths_postgrest
Exposed-Modules: PostgREST.App
@@ -134,19 +134,19 @@ Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
ghc-options: -threaded -rtsopts -with-rtsopts=-N
Hs-Source-Dirs: test, src
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
Main-Is: Main.hs
Other-Modules: Feature.AuthSpec
, Feature.ConcurrentSpec
, Feature.CorsSpec
, Feature.DeleteSpec
, Feature.InsertSpec
, Feature.QuerySpec
, Feature.QueryLimitedSpec
, Feature.RangeSpec
, Feature.StructureSpec
, Feature.UnicodeSpec
, Paths_postgrest
, PostgREST.App
, PostgREST.Auth
@@ -162,25 +162,30 @@ Test-Suite spec
, SpecHelper
, TestTypes
Build-Depends: aeson
, async
, base
, base64-string
, bytestring
, case-insensitive
, cassava
, containers
, contravariant
, errors
, hasql
, hasql-backend
, hasql-postgres
, hasql-pool
, hasql-transaction
, heredoc
, hlint
, hspec == 2.2.*
, hspec-wai
, hspec-wai-json
, http-types
, interpolatedstring-perl6
, jwt
, lens
, lens-aeson
, monad-control
, mtl
, optparse-applicative
, packdeps
, parsec
, process
, regex-tdfa
@@ -189,12 +194,15 @@ Test-Suite spec
, string-conversions
, text
, time
, transformers
, transformers-base
, unordered-containers
, unix
, vector
, wai
, wai-cors
, wai-extra
, wai-middleware-static
, warp
, HTTP
, MissingH
, Ranged-sets
+3 -4
View File
@@ -11,7 +11,6 @@ create role authenticator noinherit;
grant anon, author to authenticator;
create extension if not exists pgcrypto;
create extension if not exists "uuid-ossp";
-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
@@ -97,7 +96,7 @@ basic_auth.send_validation() returns trigger
declare
tok uuid;
begin
select uuid_generate_v4() into tok;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'validation', new.email);
perform pg_notify('validate',
@@ -175,7 +174,7 @@ begin
where token_type = 'reset'
and tokens.email = request_password_reset.email;
select uuid_generate_v4() into tok;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', request_password_reset.email);
perform pg_notify('reset',
@@ -215,7 +214,7 @@ begin
where token_type = 'reset'
and tokens.email = reset_password.email;
select uuid_generate_v4() into tok;
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'reset', reset_password.email);
perform pg_notify('reset',
+26 -15
View File
@@ -28,9 +28,11 @@ type RequestBody = BL.ByteString
data Action = ActionCreate | ActionRead
| ActionUpdate | ActionDelete
| ActionInfo | ActionInvoke
| ActionUnknown BS.ByteString deriving Eq
| ActionInappropriate
deriving Eq
-- | The target db object of a user action
data Target = TargetIdent QualifiedIdentifier
| TargetProc QualifiedIdentifier
| TargetRoot
| TargetUnknown [T.Text]
-- | How to return the inserted data
@@ -39,8 +41,8 @@ data PreferRepresentation = Full | HeadersOnly | None deriving Eq
-- route responses and upload payloads
data ContentType = ApplicationJSON | TextCSV deriving Eq
instance Show ContentType where
show ApplicationJSON = "application/json"
show TextCSV = "text/csv"
show ApplicationJSON = "application/json; charset=utf-8"
show TextCSV = "text/csv; charset=utf-8"
{-|
Describes what the user wants to do. This data type is a
@@ -77,20 +79,25 @@ data ApiRequest = ApiRequest {
-- | Examines HTTP request and translates it into user intent.
userApiRequest :: Schema -> Request -> RequestBody -> ApiRequest
userApiRequest schema req reqBody =
let action = case method of
"GET" -> ActionRead
"POST" -> if isTargetingProc
then ActionInvoke
else ActionCreate
"PATCH" -> ActionUpdate
"DELETE" -> ActionDelete
"OPTIONS" -> ActionInfo
other -> ActionUnknown other
let action =
if isTargetingProc
then
if method == "POST"
then ActionInvoke
else ActionInappropriate
else
case method of
"GET" -> ActionRead
"POST" -> ActionCreate
"PATCH" -> ActionUpdate
"DELETE" -> ActionDelete
"OPTIONS" -> ActionInfo
_ -> ActionInappropriate
target = case path of
[] -> TargetRoot
[table] -> TargetIdent
$ QualifiedIdentifier schema table
["rpc", proc] -> TargetIdent
["rpc", proc] -> TargetProc
$ QualifiedIdentifier schema proc
other -> TargetUnknown other
payload = case pickContentType (lookupHeader "content-type") of
@@ -123,7 +130,7 @@ userApiRequest schema req reqBody =
, iPayload = relevantPayload
, iPreferRepresentation = representation
, iPreferSingular = singular
, iPreferCount = not $ hasPrefer "count=none"
, iPreferCount = not $ singular || hasPrefer "count=none"
, iFilters = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
, iSelect = if method == "DELETE"
then "*"
@@ -138,7 +145,11 @@ userApiRequest schema req reqBody =
hdrs = requestHeaders req
qParams = [(cs k, cs <$> v)|(k,v) <- queryString req]
lookupHeader = flip lookup hdrs
hasPrefer val = any (\(h,v) -> h == "Prefer" && v == val) hdrs
hasPrefer :: T.Text -> Bool
hasPrefer val = any (\(h,v) -> h == "Prefer" && val `elem` split v) hdrs
where
split :: BS.ByteString -> [T.Text]
split = map T.strip . T.split (==';') . cs
singular = hasPrefer "plurality=singular"
representation
| hasPrefer "return=representation" = Full
+94 -72
View File
@@ -3,23 +3,24 @@
{-# LANGUAGE TupleSections #-}
--module PostgREST.App where
module PostgREST.App (
app
postgrest
) where
import Control.Applicative
import Control.Arrow ((***))
import Control.Monad (join)
import Data.Bifunctor (first)
import qualified Data.ByteString.Lazy as BL
import Data.Functor.Identity
import Data.List (find, sortBy, delete)
import Data.Maybe (fromMaybe, fromJust, mapMaybe)
import Data.Maybe (isJust, fromMaybe, fromJust, mapMaybe)
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange)
import Data.String.Conversions (cs)
import Data.Text (Text, replace, strip)
import Data.Tree
import qualified Hasql.Pool as P
import qualified Hasql.Transaction as HT
import Text.Parsec.Error
import Text.ParserCombinators.Parsec (parse)
@@ -28,29 +29,27 @@ import Network.HTTP.Types.Header
import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.Wai
import Network.Wai.Middleware.RequestLogger (logStdout)
import Data.Aeson
import Data.Aeson.Types (emptyArray)
import Data.Monoid
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import qualified Hasql.Transaction as H
import PostgREST.Config (AppConfig (..))
import PostgREST.Parsers
import PostgREST.DbStructure
import PostgREST.RangeQuery
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
, Action(..), Target(..)
, PreferRepresentation (..)
, userApiRequest)
import PostgREST.Types
import PostgREST.Auth (tokenJWT)
import PostgREST.Error (errResponse)
import PostgREST.QueryBuilder ( asJson
, callProc
import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure
import PostgREST.Error (errResponse, pgErrResponse)
import PostgREST.Parsers
import PostgREST.RangeQuery
import PostgREST.Middleware
import PostgREST.QueryBuilder ( callProc
, addJoinConditions
, sourceCTEName
, requestToQuery
@@ -58,11 +57,27 @@ import PostgREST.QueryBuilder ( asJson
, addRelations
, createReadStatement
, createWriteStatement
, ResultsWithCount
)
import PostgREST.Types
import Prelude
app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Tx P.Postgres s Response
postgrest :: AppConfig -> DbStructure -> P.Pool -> Application
postgrest conf dbStructure pool =
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle in
middle $ \ req respond -> do
time <- getPOSIXTime
body <- strictRequestBody req
let handleReq = runWithClaims conf time (app dbStructure conf body) req
resp <- either pgErrResponse id <$> P.use pool
(HT.run handleReq HT.ReadCommitted HT.Write)
respond resp
app :: DbStructure -> AppConfig -> RequestBody -> Request -> H.Transaction Response
app dbStructure conf reqBody req =
let
-- TODO: blow up for Left values (there is a middleware that checks the headers)
@@ -75,24 +90,18 @@ app dbStructure conf reqBody req =
case readSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (q, cq) -> do
let range = restrictRange (configMaxRows conf) $ iRange apiRequest
singular = iPreferSingular apiRequest
let singular = iPreferSingular apiRequest
stm = createReadStatement q cq range singular
(iPreferCount apiRequest) (contentType == TextCSV)
if range == emptyRange
then return $ errResponse status416 "HTTP Range error"
else do
row <- H.maybeEx stm
let (tableTotal, queryTotal, _ , body) = extractQueryResult row
shouldCount (contentType == TextCSV)
respondToRange $ do
row <- H.query () stm
let (tableTotal, queryTotal, _ , body) = row
if singular
then return $ if queryTotal <= 0
then responseLBS status404 [] ""
else responseLBS status200 [contentTypeH] (fromMaybe "{}" body)
else responseLBS status200 [contentTypeH] (cs body)
else do
let frm = rangeOffset range
to = frm+queryTotal-1
contentRange = contentRangeH frm to tableTotal
status = rangeStatus frm to tableTotal
let (status, contentRange) = rangeHeader queryTotal tableTotal
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
. sortBy (comparing fst)
. map (join (***) cs)
@@ -104,91 +113,95 @@ app dbStructure conf reqBody req =
"/" <> cs (qiName qi) <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (fromMaybe "[]" body)
] (cs body)
(ActionCreate, TargetIdent qi@(QualifiedIdentifier _ table),
Just payload@(PayloadJSON (UniformObjects rows))) ->
Just payload@(PayloadJSON uniform@(UniformObjects rows))) ->
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do
let isSingle = (==1) $ V.length rows
let pKeys = map pkName $ filter (filterPk schema table) allPrKeys -- would it be ok to move primary key detection in the query itself?
let stm = createWriteStatement qi sq mq isSingle (iPreferRepresentation apiRequest) pKeys (contentType == TextCSV) payload
row <- H.maybeEx stm
row <- H.query uniform stm
let (_, _, location, body) = extractQueryResult row
return $ responseLBS status201
[
contentTypeH,
(hLocation, "/" <> cs table <> "?" <> cs (fromMaybe "" location))
(hLocation, "/" <> cs table <> "?" <> cs location)
]
$ if iPreferRepresentation apiRequest == Full then fromMaybe "[]" body else ""
$ if iPreferRepresentation apiRequest == Full then cs body else ""
(ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON _)) ->
(ActionUpdate, TargetIdent qi, Just payload@(PayloadJSON uniform)) ->
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do
let stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) payload
row <- H.maybeEx stm
row <- H.query uniform stm
let (_, queryTotal, _, body) = extractQueryResult row
r = contentRangeH 0 (queryTotal-1) (Just queryTotal)
r = contentRangeH 0 (toInteger $ queryTotal-1) (toInteger <$> Just queryTotal)
s = case () of _ | queryTotal == 0 -> status404
| iPreferRepresentation apiRequest == Full -> status200
| otherwise -> status204
return $ responseLBS s [contentTypeH, r]
$ if iPreferRepresentation apiRequest == Full then fromMaybe "[]" body else ""
$ if iPreferRepresentation apiRequest == Full then cs body else ""
(ActionDelete, TargetIdent qi, Nothing) ->
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do
let fakeload = PayloadJSON $ UniformObjects V.empty
let emptyUniform = UniformObjects V.empty
let fakeload = PayloadJSON emptyUniform
let stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) fakeload
row <- H.maybeEx stm
row <- H.query emptyUniform stm
let (_, queryTotal, _, _) = extractQueryResult row
return $ if queryTotal == 0
then notFound
else responseLBS status204 [("Content-Range", "*/"<> cs (show queryTotal))] ""
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) -> do
let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
body = encode (TableOptions cols pkeys)
filterCol :: Schema -> TableName -> Column -> Bool
filterCol sc tb (Column{colTable=Table{tableSchema=s, tableName=t}}) = s==sc && t==tb
filterCol _ _ _ = False
return $ responseLBS status200 [jsonH, allOrigins] $ cs body
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) ->
if isJust $ find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure)
then let cols = filter (filterCol tSchema tTable) $ dbColumns dbStructure
pkeys = map pkName $ filter (filterPk tSchema tTable) allPrKeys
body = encode (TableOptions cols pkeys)
filterCol :: Schema -> TableName -> Column -> Bool
filterCol sc tb Column{colTable=Table{tableSchema=s, tableName=t}} = s==sc && t==tb
filterCol _ _ _ = False in
return $ responseLBS status200 [jsonH, allOrigins] $ cs body
else
return notFound
(ActionInvoke, TargetIdent qi,
(ActionInvoke, TargetProc qi,
Just (PayloadJSON (UniformObjects payload))) -> do
exists <- doesProcExist qi
exists <- H.query qi doesProcExist
if exists
then do
let p = V.head payload
call = B.Stmt "select " V.empty True <>
asJson (callProc qi p)
jwtSecret = configJwtSecret conf
bodyJson :: Maybe (Identity Value) <- H.maybeEx call
returnJWT <- doesProcReturnJWT qi
return $ responseLBS status200 [jsonH]
(let body = fromMaybe emptyArray $ runIdentity <$> bodyJson in
if returnJWT
then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}"
else cs $ encode body)
respondToRange $ do
row <- H.query () (callProc qi p range shouldCount)
returnJWT <- H.query qi doesProcReturnJWT
let (tableTotal, queryTotal, body) = fromMaybe (Just 0, 0, emptyArray) row
(status, contentRange) = rangeHeader queryTotal tableTotal
in
return $ responseLBS status [jsonH, contentRange]
(if returnJWT
then "{\"token\":\"" <> cs (tokenJWT jwtSecret body) <> "\"}"
else cs $ encode body)
else return notFound
(ActionRead, TargetRoot, Nothing) -> do
body <- encode <$> accessibleTables (filter ((== cs schema) . tableSchema) (dbTables dbStructure))
body <- encode <$> H.query schema accessibleTables
return $ responseLBS status200 [jsonH] $ cs body
(ActionUnknown _, _, _) -> return notFound
(_, TargetUnknown _, _) -> return notFound
(ActionInappropriate, _, _) -> return $ responseLBS status405 [] ""
(_, _, Just (PayloadParseError e)) ->
return $ responseLBS status400 [jsonH] $
cs (formatGeneralError "Cannot parse request payload" (cs e))
(_, TargetUnknown _, _) -> return notFound
(_, _, _) -> return notFound
where
@@ -198,6 +211,8 @@ app dbStructure conf reqBody req =
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
schema = cs $ configSchema conf
apiRequest = userApiRequest schema req reqBody
shouldCount = iPreferCount apiRequest
range = restrictRange (configMaxRows conf) $ iRange apiRequest
readDbRequest = DbRead <$> buildReadRequest (dbRelations dbStructure) apiRequest
mutateDbRequest = DbMutate <$> buildMutateRequest apiRequest
selectQuery = requestToQuery schema <$> readDbRequest
@@ -205,15 +220,23 @@ app dbStructure conf reqBody req =
mutateQuery = requestToQuery schema <$> mutateDbRequest
readSqlParts = (,) <$> selectQuery <*> countQuery
mutateSqlParts = (,) <$> selectQuery <*> mutateQuery
respondToRange response = if range == emptyRange
then return $ errResponse status416 "HTTP Range error"
else response
rangeHeader queryTotal tableTotal = let frm = rangeOffset range
to = frm + toInteger queryTotal - 1
contentRange = contentRangeH frm to (toInteger <$> tableTotal)
status = rangeStatus frm to (toInteger <$> tableTotal)
in (status, contentRange)
rangeStatus :: Int -> Int -> Maybe Int -> Status
rangeStatus :: Integer -> Integer -> Maybe Integer -> Status
rangeStatus _ _ Nothing = status200
rangeStatus frm to (Just total)
| frm > total = status416
| (1 + to - frm) < total = status206
| otherwise = status200
contentRangeH :: Int -> Int -> Maybe Int -> Header
contentRangeH :: Integer -> Integer -> Maybe Integer -> Header
contentRangeH frm to total =
("Content-Range", cs headerValue)
where
@@ -226,7 +249,7 @@ contentRangeH frm to total =
fromInRange = frm <= to
jsonH :: Header
jsonH = (hContentType, "application/json")
jsonH = (hContentType, "application/json; charset=utf-8")
formatRelationError :: Text -> Text
formatRelationError = formatGeneralError
@@ -298,7 +321,7 @@ buildMutateRequest apiRequest =
cond = first formatParserError $ map snd <$> mapM pRequestFilter mutateFilters
addFilter :: (Path, Filter) -> ReadRequest -> ReadRequest
addFilter ([], flt) (Node (q@(Select {flt_=flts}), i) forest) = Node (q {flt_=flt:flts}, i) forest
addFilter ([], flt) (Node (q@Select {flt_=flts}, i) forest) = Node (q {flt_=flt:flts}, i) forest
addFilter (path, flt) (Node rn forest) =
case targetNode of
Nothing -> Node rn forest -- the filter is silenty dropped in the Request does not contain the required path
@@ -335,6 +358,5 @@ instance ToJSON TableOptions where
, "pkey" .= tblOptpkey t ]
extractQueryResult :: Maybe (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString)
-> (Maybe Int, Int, Maybe BL.ByteString, Maybe BL.ByteString)
extractQueryResult = fromMaybe (Just 0, 0, Just "", Just "")
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
extractQueryResult = fromMaybe (Nothing, 0, "", "")
+37 -34
View File
@@ -18,18 +18,20 @@ module PostgREST.Auth (
, tokenJWT
) where
import Control.Monad (join)
import Data.Aeson (Value (..), Object)
import Data.Aeson.Types (emptyObject, emptyArray)
import Data.Vector as V (null, head)
import Data.Map as M (fromList, toList)
import Control.Lens
import Data.Aeson (Value (..), parseJSON, toJSON)
import Data.Aeson.Lens
import Data.Aeson.Types (parseMaybe, emptyObject, emptyArray)
import qualified Data.ByteString as BS
import qualified Data.Vector as V
import qualified Data.HashMap.Strict as M
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Time.Clock (NominalDiffTime)
import PostgREST.QueryBuilder (pgFmtLit, pgFmtIdent, unquoted)
import PostgREST.QueryBuilder (pgFmtIdent, pgFmtLit, unquoted)
import qualified Web.JWT as JWT
import qualified Data.HashMap.Lazy as H
{-|
Receives a map of JWT claims and returns a list
@@ -38,12 +40,12 @@ import qualified Data.HashMap.Lazy as H
this one is mapped to a SET ROLE statement.
In case there is any problem decoding the JWT it returns Nothing.
-}
claimsToSQL :: JWT.ClaimsMap -> [Text]
claimsToSQL = map setVar . toList
claimsToSQL :: M.HashMap Text Value -> [BS.ByteString]
claimsToSQL = map setVar . M.toList
where
setVar ("role", String val) = setRole val
setVar (k, val) = "set local postgrest.claims." <> pgFmtIdent k <>
" = " <> valueToVariable val <> ";"
setVar (k, val) = "set local " <> cs (pgFmtIdent $ "postgrest.claims." <> k)
<> " = " <> cs (valueToVariable val) <> ";"
valueToVariable = pgFmtLit . unquoted
{-|
@@ -51,23 +53,26 @@ claimsToSQL = map setVar . toList
returns a map of JWT claims
In case there is any problem decoding the JWT it returns Nothing.
-}
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Maybe JWT.ClaimsMap
jwtClaims secret input time =
case join $ claim JWT.exp of
Just expires ->
if JWT.secondsSinceEpoch expires > time
then customClaims
else Nothing
_ -> customClaims
where
decoded = JWT.decodeAndVerifySignature secret input
claim :: (JWT.JWTClaimsSet -> a) -> Maybe a
claim prop = prop . JWT.claims <$> decoded
customClaims = claim JWT.unregisteredClaims
-- | Receives the name of a role and returns a SET ROLE statement
setRole :: Text -> Text
setRole role = "set local role " <> cs (pgFmtLit role) <> ";"
jwtClaims :: JWT.Secret -> Text -> NominalDiffTime -> Either Text (M.HashMap Text Value)
jwtClaims secret input time =
case mClaims of
Nothing -> Right M.empty
Just claims -> do
let mExp = claims ^? key "exp" . _Integer
expired = fromMaybe False $ (<= time) . fromInteger <$> mExp
if expired
then Left "JWT expired"
else Right (value2map claims)
where
mClaims = toJSON . JWT.claims <$> JWT.decodeAndVerifySignature secret input
value2map (Object o) = o
value2map _ = M.empty
{-| Receives the name of a role and returns a SET ROLE statement -}
setRole :: Text -> BS.ByteString
setRole r = "set local role " <> cs (pgFmtLit r) <> ";"
{-|
@@ -75,10 +80,8 @@ setRole role = "set local role " <> cs (pgFmtLit role) <> ";"
and returns a signed JWT.
-}
tokenJWT :: JWT.Secret -> Value -> Text
tokenJWT secret (Array a) = JWT.encodeSigned JWT.HS256 secret
JWT.def { JWT.unregisteredClaims = fromHashMap o }
where
Object o = if V.null a then emptyObject else V.head a
fromHashMap :: Object -> JWT.ClaimsMap
fromHashMap = M.fromList . H.toList
tokenJWT secret _ = tokenJWT secret emptyArray
tokenJWT secret (Array arr) =
let obj = if V.null arr then emptyObject else V.head arr
jcs = parseMaybe parseJSON obj :: Maybe JWT.JWTClaimsSet in
JWT.encodeSigned JWT.HS256 secret $ fromMaybe JWT.def jcs
tokenJWT secret _ = tokenJWT secret emptyArray
+8 -7
View File
@@ -30,32 +30,33 @@ import Network.Wai
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative
import Paths_postgrest (version)
import Prelude
import Safe (readMay)
import Web.JWT (Secret, secret)
import Prelude
-- | Data type to store all command line options
data AppConfig = AppConfig {
configDatabase :: String
, configPort :: Int
, configAnonRole :: String
, configSchema :: String
, configPort :: Int
, configJwtSecret :: Secret
, configPool :: Int
, configMaxRows :: Maybe Int
, configMaxRows :: Maybe Integer
, configQuiet :: Bool
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> argument str (help "database connection string" <> metavar "STRING")
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
<*> strOption (long "anonymous" <> short 'a' <> help "postgres role to use for non-authenticated requests" <> metavar "ROLE")
<$> argument str (help "(REQUIRED) database connection string, e.g. postgres://user:pass@host:port/db" <> metavar "DB_URL")
<*> strOption (long "anonymous" <> short 'a' <> help "(REQUIRED) postgres role to use for non-authenticated requests" <> metavar "ROLE")
<*> strOption (long "schema" <> short 's' <> help "schema to use for API routes" <> metavar "NAME" <> value "public" <> showDefault)
<*> option auto (long "port" <> short 'p' <> help "port number on which to run HTTP server" <> metavar "PORT" <> value 3000 <> showDefault)
<*> (secret . cs <$>
strOption (long "jwt-secret" <> short 'j' <> help "secret used to encrypt and decrypt JWT tokens" <> metavar "SECRET" <> value "secret" <> showDefault))
<*> option auto (long "pool" <> short 'o' <> help "max connections in database pool" <> metavar "COUNT" <> value 10 <> showDefault)
<*> (readMay <$> strOption (long "max-rows" <> short 'm' <> help "max rows in response" <> metavar "COUNT" <> value "infinity" <> showDefault))
<*> pure False
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
+465 -394
View File
@@ -10,28 +10,34 @@ module PostgREST.DbStructure (
, doesProcReturnJWT
) where
import Control.Applicative
import Control.Monad (join)
import Data.Functor.Identity
import Data.List (elemIndex, find, subsequences, sort, transpose)
import Data.Maybe (fromMaybe, fromJust, isJust, mapMaybe, listToMaybe)
import Data.Monoid
import Data.Text (Text, split)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Hasql.Backend as B
import PostgREST.Types
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Query as H
import GHC.Exts (groupWith)
import Control.Applicative
import Control.Monad (join, replicateM)
import Data.Functor.Contravariant (contramap)
import Data.List (elemIndex, find, sort,
subsequences, transpose)
import Data.Maybe (fromJust, fromMaybe, isJust,
listToMaybe, mapMaybe)
import Data.Monoid
import Data.Text (Text, split)
import qualified Hasql.Session as H
import PostgREST.Types
import Text.InterpolatedString.Perl6 (q)
import Data.Int (Int32)
import GHC.Exts (groupWith)
import Prelude
getDbStructure :: Schema -> H.Tx P.Postgres s DbStructure
getDbStructure :: Schema -> H.Session DbStructure
getDbStructure schema = do
tabs <- allTables
cols <- allColumns tabs
syns <- allSynonyms cols
rels <- allRelations tabs cols
keys <- allPrimaryKeys tabs
tabs <- H.query () allTables
cols <- H.query () $ allColumns tabs
syns <- H.query () $ allSynonyms cols
rels <- H.query () $ allRelations tabs cols
keys <- H.query () $ allPrimaryKeys tabs
let rels' = (addManyToManyRelations . raiseRelations schema syns . addParentRelations . addSynonymousRelations syns) rels
cols' = addForeignKeys rels' cols
@@ -44,52 +50,113 @@ getDbStructure schema = do
, dbPrimaryKeys = keys'
}
doesProc :: forall c s. B.CxValue c Int =>
(Text -> Text -> B.Stmt c) -> QualifiedIdentifier -> H.Tx c s Bool
doesProc stmt qi = do
row :: Maybe (Identity Int) <- H.maybeEx $ stmt (qiSchema qi) (qiName qi)
return $ isJust row
encodeQi :: HE.Params QualifiedIdentifier
encodeQi =
contramap qiSchema (HE.value HE.text) <>
contramap qiName (HE.value HE.text)
doesProcExist :: QualifiedIdentifier -> H.Tx P.Postgres s Bool
doesProcExist = doesProc [H.stmt|
decodeTables :: HD.Result [Table]
decodeTables =
HD.rowsList tblRow
where
tblRow = Table <$> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.bool
decodeColumns :: [Table] -> HD.Result [Column]
decodeColumns tables =
mapMaybe (columnFromRow tables) <$> HD.rowsList colRow
where
colRow =
(,,,,,,,,,,)
<$> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.text <*> HD.value HD.int4
<*> HD.value HD.bool <*> HD.value HD.text
<*> HD.value HD.bool
<*> HD.nullableValue HD.int4
<*> HD.nullableValue HD.int4
<*> HD.nullableValue HD.text
<*> HD.nullableValue HD.text
decodeRelations :: [Table] -> [Column] -> HD.Result [Relation]
decodeRelations tables cols =
mapMaybe (relationFromRow tables cols) <$> HD.rowsList relRow
where
relRow = (,,,,,)
<$> HD.value HD.text
<*> HD.value HD.text
<*> HD.value (HD.array (HD.arrayDimension replicateM (HD.arrayValue HD.text)))
<*> HD.value HD.text
<*> HD.value HD.text
<*> HD.value (HD.array (HD.arrayDimension replicateM (HD.arrayValue HD.text)))
decodePks :: [Table] -> HD.Result [PrimaryKey]
decodePks tables =
mapMaybe (pkFromRow tables) <$> HD.rowsList pkRow
where
pkRow = (,,) <$> HD.value HD.text <*> HD.value HD.text <*> HD.value HD.text
decodeSynonyms :: [Column] -> HD.Result [(Column,Column)]
decodeSynonyms cols =
mapMaybe (synonymFromRow cols) <$> HD.rowsList synRow
where
synRow = (,,,,,)
<$> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.text <*> HD.value HD.text
<*> HD.value HD.text <*> HD.value HD.text
doesProcExist :: H.Query QualifiedIdentifier Bool
doesProcExist =
H.statement sql encodeQi (HD.singleRow (HD.value HD.bool)) True
where
sql = [q| SELECT EXISTS (
SELECT 1
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p
ON pronamespace = n.oid
WHERE nspname = ?
AND proname = ?
|]
WHERE nspname = $1
AND proname = $2
) |]
doesProcReturnJWT :: QualifiedIdentifier -> H.Tx P.Postgres s Bool
doesProcReturnJWT = doesProc [H.stmt|
doesProcReturnJWT :: H.Query QualifiedIdentifier Bool
doesProcReturnJWT =
H.statement sql encodeQi (HD.singleRow (HD.value HD.bool)) True
where
sql = [q| SELECT EXISTS (
SELECT 1
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p
ON pronamespace = n.oid
WHERE nspname = ?
AND proname = ?
WHERE nspname = $1
AND proname = $2
AND pg_catalog.pg_get_function_result(p.oid) like '%jwt_claims'
|]
) |]
accessibleTables :: [Table] -> H.Tx P.Postgres s [Table]
accessibleTables allTabs = do
accessible <- H.listEx $ [H.stmt|
SELECT
n.nspname AS table_schema,
c.relname AS table_name
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE
c.relkind IN ('v','r','m') AND
n.nspname NOT IN ('pg_catalog', 'information_schema') AND (
pg_has_role(c.relowner, 'USAGE'::text) OR
has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR
has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)
)
ORDER BY table_schema, table_name
|]
let isAccessible table = isJust $ find (\(s,n) -> tableSchema table == s && tableName table == n) accessible
return $ filter isAccessible allTabs
accessibleTables :: H.Query Schema [Table]
accessibleTables =
H.statement sql (HE.value HE.text) decodeTables True
where
sql = [q|
select
n.nspname as table_schema,
relname as table_name,
c.relkind = 'r' or (c.relkind IN ('v', 'f')) and (pg_relation_is_updatable(c.oid::regclass, false) & 8) = 8
or (exists (
select 1
from pg_trigger
where pg_trigger.tgrelid = c.oid and (pg_trigger.tgtype::integer & 69) = 69)
) as insertable
from
pg_class c
join pg_namespace n on n.oid = c.relnamespace
where
c.relkind in ('v', 'r', 'm')
and n.nspname = $1
and (
pg_has_role(c.relowner, 'USAGE'::text)
or has_table_privilege(c.oid, 'SELECT, INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text)
or has_any_column_privilege(c.oid, 'SELECT, INSERT, UPDATE, REFERENCES'::text)
)
order by relname |]
synonymousColumns :: [(Column,Column)] -> [Column] -> [[Column]]
synonymousColumns allSyns cols = synCols'
@@ -107,9 +174,9 @@ addForeignKeys rels = map addFk
addFk col = col { colFK = fk col }
fk col = join $ relToFk col <$> find (lookupFn col) rels
lookupFn :: Column -> Relation -> Bool
lookupFn c (Relation{relColumns=cs, relType=rty}) = c `elem` cs && rty==Child
lookupFn c Relation{relColumns=cs, relType=rty} = c `elem` cs && rty==Child
-- lookupFn _ _ = False
relToFk col (Relation{relColumns=cols, relFColumns=colsF}) = ForeignKey <$> colF
relToFk col Relation{relColumns=cols, relFColumns=colsF} = ForeignKey <$> colF
where
pos = elemIndex col cols
colF = (colsF !!) <$> pos
@@ -131,7 +198,7 @@ addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation
where
links = join $ map (combinations 2) $ filter (not . null) $ groupWith groupFn $ filter ( (==Child). relType) rels
groupFn :: Relation -> Text
groupFn (Relation{relTable=Table{tableSchema=s, tableName=t}}) = s<>"_"<>t
groupFn Relation{relTable=Table{tableSchema=s, tableName=t}} = s<>"_"<>t
combinations k ns = filter ((k==).length) (subsequences ns)
addMirrorRelation [] = []
addMirrorRelation (rel@(Relation t c ft fc _ lt lc1 lc2):rels') = Relation ft fc t c Many lt lc2 lc1 : rel : addMirrorRelation rels'
@@ -163,173 +230,170 @@ synonymousPrimaryKeys syns (key:keys) = key : newKeys ++ synonymousPrimaryKeys s
keySyns = filter ((\c -> colTable c == pkTable key && colName c == pkName key) . fst) syns
newKeys = map ((\c -> PrimaryKey{pkTable=colTable c,pkName=colName c}) . snd) keySyns
allTables :: H.Tx P.Postgres s [Table]
allTables = do
rows <- H.listEx $ [H.stmt|
SELECT
n.nspname AS table_schema,
c.relname AS table_name,
c.relkind = 'r' OR (c.relkind IN ('v','f'))
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
OR (EXISTS
( SELECT 1
FROM pg_trigger
WHERE pg_trigger.tgrelid = c.oid
AND (pg_trigger.tgtype::integer & 69) = 69) ) AS insertable
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('v','r','m')
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
GROUP BY table_schema, table_name, insertable
ORDER BY table_schema, table_name
|]
return $ map tableFromRow rows
allTables :: H.Query () [Table]
allTables =
H.statement sql HE.unit decodeTables True
where
sql = [q|
SELECT
n.nspname AS table_schema,
c.relname AS table_name,
c.relkind = 'r' OR (c.relkind IN ('v','f'))
AND (pg_relation_is_updatable(c.oid::regclass, FALSE) & 8) = 8
OR (EXISTS
( SELECT 1
FROM pg_trigger
WHERE pg_trigger.tgrelid = c.oid
AND (pg_trigger.tgtype::integer & 69) = 69) ) AS insertable
FROM pg_class c
JOIN pg_namespace n ON n.oid = c.relnamespace
WHERE c.relkind IN ('v','r','m')
AND n.nspname NOT IN ('pg_catalog', 'information_schema')
GROUP BY table_schema, table_name, insertable
ORDER BY table_schema, table_name |]
tableFromRow :: (Text, Text, Bool) -> Table
tableFromRow (s, n, i) = Table s n i
allColumns :: [Table] -> H.Tx P.Postgres s [Column]
allColumns tabs = do
cols <- H.listEx $ [H.stmt|
SELECT DISTINCT
info.table_schema AS schema,
info.table_name AS table_name,
info.column_name AS name,
info.ordinal_position AS position,
info.is_nullable::boolean AS nullable,
info.data_type AS col_type,
info.is_updatable::boolean 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 (
/*
-- CTE based on information_schema.columns to remove the owner filter
*/
WITH columns AS (
SELECT current_database()::information_schema.sql_identifier AS table_catalog,
nc.nspname::information_schema.sql_identifier AS table_schema,
c.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
a.attnum::information_schema.cardinal_number AS ordinal_position,
pg_get_expr(ad.adbin, ad.adrelid)::information_schema.character_data AS column_default,
CASE
WHEN a.attnotnull OR t.typtype = 'd'::"char" AND t.typnotnull THEN 'NO'::text
ELSE 'YES'::text
END::information_schema.yes_or_no AS is_nullable,
CASE
WHEN t.typtype = 'd'::"char" THEN
CASE
WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text
WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)
ELSE 'USER-DEFINED'::text
END
ELSE
CASE
WHEN t.typelem <> 0::oid AND t.typlen = (-1) THEN 'ARRAY'::text
WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)
ELSE 'USER-DEFINED'::text
END
END::information_schema.character_data AS data_type,
information_schema._pg_char_max_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_maximum_length,
information_schema._pg_char_octet_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_octet_length,
information_schema._pg_numeric_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision,
information_schema._pg_numeric_precision_radix(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision_radix,
information_schema._pg_numeric_scale(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_scale,
information_schema._pg_datetime_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS datetime_precision,
information_schema._pg_interval_type(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.character_data AS interval_type,
NULL::integer::information_schema.cardinal_number AS interval_precision,
NULL::character varying::information_schema.sql_identifier AS character_set_catalog,
NULL::character varying::information_schema.sql_identifier AS character_set_schema,
NULL::character varying::information_schema.sql_identifier AS character_set_name,
CASE
WHEN nco.nspname IS NOT NULL THEN current_database()
ELSE NULL::name
END::information_schema.sql_identifier AS collation_catalog,
nco.nspname::information_schema.sql_identifier AS collation_schema,
co.collname::information_schema.sql_identifier AS collation_name,
CASE
WHEN t.typtype = 'd'::"char" THEN current_database()
ELSE NULL::name
END::information_schema.sql_identifier AS domain_catalog,
CASE
WHEN t.typtype = 'd'::"char" THEN nt.nspname
ELSE NULL::name
END::information_schema.sql_identifier AS domain_schema,
CASE
WHEN t.typtype = 'd'::"char" THEN t.typname
ELSE NULL::name
END::information_schema.sql_identifier AS domain_name,
current_database()::information_schema.sql_identifier AS udt_catalog,
COALESCE(nbt.nspname, nt.nspname)::information_schema.sql_identifier AS udt_schema,
COALESCE(bt.typname, t.typname)::information_schema.sql_identifier AS udt_name,
NULL::character varying::information_schema.sql_identifier AS scope_catalog,
NULL::character varying::information_schema.sql_identifier AS scope_schema,
NULL::character varying::information_schema.sql_identifier AS scope_name,
NULL::integer::information_schema.cardinal_number AS maximum_cardinality,
a.attnum::information_schema.sql_identifier AS dtd_identifier,
'NO'::character varying::information_schema.yes_or_no AS is_self_referencing,
'NO'::character varying::information_schema.yes_or_no AS is_identity,
NULL::character varying::information_schema.character_data AS identity_generation,
NULL::character varying::information_schema.character_data AS identity_start,
NULL::character varying::information_schema.character_data AS identity_increment,
NULL::character varying::information_schema.character_data AS identity_maximum,
NULL::character varying::information_schema.character_data AS identity_minimum,
NULL::character varying::information_schema.yes_or_no AS identity_cycle,
'NEVER'::character varying::information_schema.character_data AS is_generated,
NULL::character varying::information_schema.character_data AS generation_expression,
CASE
WHEN c.relkind = 'r'::"char" OR (c.relkind = ANY (ARRAY['v'::"char", 'f'::"char"])) AND pg_column_is_updatable(c.oid::regclass, a.attnum, false) THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS is_updatable
FROM pg_attribute a
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
JOIN (pg_class c
JOIN pg_namespace nc ON c.relnamespace = nc.oid) ON a.attrelid = c.oid
JOIN (pg_type t
JOIN pg_namespace nt ON t.typnamespace = nt.oid) ON a.atttypid = t.oid
LEFT JOIN (pg_type bt
JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid) ON t.typtype = 'd'::"char" AND t.typbasetype = bt.oid
LEFT JOIN (pg_collation co
JOIN pg_namespace nco ON co.collnamespace = nco.oid) ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)
WHERE NOT pg_is_other_temp_schema(nc.oid) AND a.attnum > 0 AND NOT a.attisdropped AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
/*--AND (pg_has_role(c.relowner, 'USAGE'::text) OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
)
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*/
FROM columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
) 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 schema, position
|]
return $ mapMaybe (columnFromRow tabs) cols
allColumns :: [Table] -> H.Query () [Column]
allColumns tabs =
H.statement sql HE.unit (decodeColumns tabs) True
where
sql = [q|
SELECT DISTINCT
info.table_schema AS schema,
info.table_name AS table_name,
info.column_name AS name,
info.ordinal_position AS position,
info.is_nullable::boolean AS nullable,
info.data_type AS col_type,
info.is_updatable::boolean 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 (
/*
-- CTE based on information_schema.columns to remove the owner filter
*/
WITH columns AS (
SELECT current_database()::information_schema.sql_identifier AS table_catalog,
nc.nspname::information_schema.sql_identifier AS table_schema,
c.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
a.attnum::information_schema.cardinal_number AS ordinal_position,
pg_get_expr(ad.adbin, ad.adrelid)::information_schema.character_data AS column_default,
CASE
WHEN a.attnotnull OR t.typtype = 'd'::"char" AND t.typnotnull THEN 'NO'::text
ELSE 'YES'::text
END::information_schema.yes_or_no AS is_nullable,
CASE
WHEN t.typtype = 'd'::"char" THEN
CASE
WHEN bt.typelem <> 0::oid AND bt.typlen = (-1) THEN 'ARRAY'::text
WHEN nbt.nspname = 'pg_catalog'::name THEN format_type(t.typbasetype, NULL::integer)
ELSE 'USER-DEFINED'::text
END
ELSE
CASE
WHEN t.typelem <> 0::oid AND t.typlen = (-1) THEN 'ARRAY'::text
WHEN nt.nspname = 'pg_catalog'::name THEN format_type(a.atttypid, NULL::integer)
ELSE 'USER-DEFINED'::text
END
END::information_schema.character_data AS data_type,
information_schema._pg_char_max_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_maximum_length,
information_schema._pg_char_octet_length(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS character_octet_length,
information_schema._pg_numeric_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision,
information_schema._pg_numeric_precision_radix(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_precision_radix,
information_schema._pg_numeric_scale(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS numeric_scale,
information_schema._pg_datetime_precision(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.cardinal_number AS datetime_precision,
information_schema._pg_interval_type(information_schema._pg_truetypid(a.*, t.*), information_schema._pg_truetypmod(a.*, t.*))::information_schema.character_data AS interval_type,
NULL::integer::information_schema.cardinal_number AS interval_precision,
NULL::character varying::information_schema.sql_identifier AS character_set_catalog,
NULL::character varying::information_schema.sql_identifier AS character_set_schema,
NULL::character varying::information_schema.sql_identifier AS character_set_name,
CASE
WHEN nco.nspname IS NOT NULL THEN current_database()
ELSE NULL::name
END::information_schema.sql_identifier AS collation_catalog,
nco.nspname::information_schema.sql_identifier AS collation_schema,
co.collname::information_schema.sql_identifier AS collation_name,
CASE
WHEN t.typtype = 'd'::"char" THEN current_database()
ELSE NULL::name
END::information_schema.sql_identifier AS domain_catalog,
CASE
WHEN t.typtype = 'd'::"char" THEN nt.nspname
ELSE NULL::name
END::information_schema.sql_identifier AS domain_schema,
CASE
WHEN t.typtype = 'd'::"char" THEN t.typname
ELSE NULL::name
END::information_schema.sql_identifier AS domain_name,
current_database()::information_schema.sql_identifier AS udt_catalog,
COALESCE(nbt.nspname, nt.nspname)::information_schema.sql_identifier AS udt_schema,
COALESCE(bt.typname, t.typname)::information_schema.sql_identifier AS udt_name,
NULL::character varying::information_schema.sql_identifier AS scope_catalog,
NULL::character varying::information_schema.sql_identifier AS scope_schema,
NULL::character varying::information_schema.sql_identifier AS scope_name,
NULL::integer::information_schema.cardinal_number AS maximum_cardinality,
a.attnum::information_schema.sql_identifier AS dtd_identifier,
'NO'::character varying::information_schema.yes_or_no AS is_self_referencing,
'NO'::character varying::information_schema.yes_or_no AS is_identity,
NULL::character varying::information_schema.character_data AS identity_generation,
NULL::character varying::information_schema.character_data AS identity_start,
NULL::character varying::information_schema.character_data AS identity_increment,
NULL::character varying::information_schema.character_data AS identity_maximum,
NULL::character varying::information_schema.character_data AS identity_minimum,
NULL::character varying::information_schema.yes_or_no AS identity_cycle,
'NEVER'::character varying::information_schema.character_data AS is_generated,
NULL::character varying::information_schema.character_data AS generation_expression,
CASE
WHEN c.relkind = 'r'::"char" OR (c.relkind = ANY (ARRAY['v'::"char", 'f'::"char"])) AND pg_column_is_updatable(c.oid::regclass, a.attnum, false) THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS is_updatable
FROM pg_attribute a
LEFT JOIN pg_attrdef ad ON a.attrelid = ad.adrelid AND a.attnum = ad.adnum
JOIN (pg_class c
JOIN pg_namespace nc ON c.relnamespace = nc.oid) ON a.attrelid = c.oid
JOIN (pg_type t
JOIN pg_namespace nt ON t.typnamespace = nt.oid) ON a.atttypid = t.oid
LEFT JOIN (pg_type bt
JOIN pg_namespace nbt ON bt.typnamespace = nbt.oid) ON t.typtype = 'd'::"char" AND t.typbasetype = bt.oid
LEFT JOIN (pg_collation co
JOIN pg_namespace nco ON co.collnamespace = nco.oid) ON a.attcollation = co.oid AND (nco.nspname <> 'pg_catalog'::name OR co.collname <> 'default'::name)
WHERE NOT pg_is_other_temp_schema(nc.oid) AND a.attnum > 0 AND NOT a.attisdropped AND (c.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
/*--AND (pg_has_role(c.relowner, 'USAGE'::text) OR has_column_privilege(c.oid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
)
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*/
FROM columns
WHERE table_schema NOT IN ('pg_catalog', 'information_schema')
) 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 schema, position |]
columnFromRow :: [Table] ->
(Text, Text, Text,
Int, Bool, Text,
Bool, Maybe Int, Maybe Int,
(Text, Text, Text,
Int32, Bool, Text,
Bool, Maybe Int32, Maybe Int32,
Maybe Text, Maybe Text)
-> Maybe Column
columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = buildColumn <$> table
@@ -339,9 +403,11 @@ columnFromRow tabs (s, t, n, pos, nul, typ, u, l, p, d, e) = buildColumn <$> tab
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
allRelations :: [Table] -> [Column] -> H.Tx P.Postgres s [Relation]
allRelations tabs cols = do
rels <- H.listEx $ [H.stmt|
allRelations :: [Table] -> [Column] -> H.Query () [Relation]
allRelations tabs cols =
H.statement sql HE.unit (decodeRelations tabs cols) True
where
sql = [q|
SELECT ns1.nspname AS table_schema,
tab.relname AS table_name,
column_info.cols AS columns,
@@ -365,9 +431,7 @@ allRelations tabs cols = do
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = confrelid) AS other,
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = other.relnamespace) AS ns2
WHERE confrelid != 0
ORDER BY (conrelid, column_info.nums)
|]
return $ mapMaybe (relationFromRow tabs cols) rels
ORDER BY (conrelid, column_info.nums) |]
relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
@@ -380,183 +444,190 @@ relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
cols = mapM (findCol rs rt) rcs
colsF = mapM (findCol frs frt) frcs
allPrimaryKeys :: [Table] -> H.Tx P.Postgres s [PrimaryKey]
allPrimaryKeys tabs = do
pks <- H.listEx $ [H.stmt|
/*
-- CTE to replace information_schema.table_constraints to remove owner limit
*/
WITH tc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
nc.nspname::information_schema.sql_identifier AS constraint_schema,
c.conname::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
nr.nspname::information_schema.sql_identifier AS table_schema,
r.relname::information_schema.sql_identifier AS table_name,
CASE c.contype
WHEN 'c'::"char" THEN 'CHECK'::text
WHEN 'f'::"char" THEN 'FOREIGN KEY'::text
WHEN 'p'::"char" THEN 'PRIMARY KEY'::text
WHEN 'u'::"char" THEN 'UNIQUE'::text
ELSE NULL::text
END::information_schema.character_data AS constraint_type,
CASE
WHEN c.condeferrable THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS is_deferrable,
CASE
WHEN c.condeferred THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS initially_deferred
FROM pg_namespace nc,
pg_namespace nr,
pg_constraint c,
pg_class r
WHERE nc.oid = c.connamespace AND nr.oid = r.relnamespace AND c.conrelid = r.oid AND (c.contype <> ALL (ARRAY['t'::"char", 'x'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)
/*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/
UNION ALL
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
nr.nspname::information_schema.sql_identifier AS constraint_schema,
(((((nr.oid::text || '_'::text) || r.oid::text) || '_'::text) || a.attnum::text) || '_not_null'::text)::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
nr.nspname::information_schema.sql_identifier AS table_schema,
r.relname::information_schema.sql_identifier AS table_name,
'CHECK'::character varying::information_schema.character_data AS constraint_type,
'NO'::character varying::information_schema.yes_or_no AS is_deferrable,
'NO'::character varying::information_schema.yes_or_no AS initially_deferred
FROM pg_namespace nr,
pg_class r,
pg_attribute a
WHERE nr.oid = r.relnamespace AND r.oid = a.attrelid AND a.attnotnull AND a.attnum > 0 AND NOT a.attisdropped AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)
/*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/
),
/*
-- CTE to replace information_schema.key_column_usage to remove owner limit
*/
kc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
ss.nc_nspname::information_schema.sql_identifier AS constraint_schema,
ss.conname::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
ss.nr_nspname::information_schema.sql_identifier AS table_schema,
ss.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
(ss.x).n::information_schema.cardinal_number AS ordinal_position,
CASE
WHEN ss.contype = 'f'::"char" THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])
ELSE NULL::integer
END::information_schema.cardinal_number AS position_in_unique_constraint
FROM pg_attribute a,
( SELECT r.oid AS roid,
r.relname,
r.relowner,
nc.nspname AS nc_nspname,
nr.nspname AS nr_nspname,
c.oid AS coid,
c.conname,
c.contype,
c.conindid,
c.confkey,
c.confrelid,
information_schema._pg_expandarray(c.conkey) AS x
FROM pg_namespace nr,
pg_class r,
pg_namespace nc,
pg_constraint c
WHERE nr.oid = r.relnamespace AND r.oid = c.conrelid AND nc.oid = c.connamespace AND (c.contype = ANY (ARRAY['p'::"char", 'u'::"char", 'f'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)) ss
WHERE ss.roid = a.attrelid AND a.attnum = (ss.x).x AND NOT a.attisdropped
/*--AND (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
)
SELECT
kc.table_schema,
kc.table_name,
kc.column_name
FROM
/*
--information_schema.table_constraints tc,
--information_schema.key_column_usage kc
*/
tc, 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 NOT IN ('pg_catalog', 'information_schema')
|]
return $ mapMaybe (pkFromRow tabs) pks
allPrimaryKeys :: [Table] -> H.Query () [PrimaryKey]
allPrimaryKeys tabs =
H.statement sql HE.unit (decodePks tabs) True
where
sql = [q|
/*
-- CTE to replace information_schema.table_constraints to remove owner limit
*/
WITH tc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
nc.nspname::information_schema.sql_identifier AS constraint_schema,
c.conname::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
nr.nspname::information_schema.sql_identifier AS table_schema,
r.relname::information_schema.sql_identifier AS table_name,
CASE c.contype
WHEN 'c'::"char" THEN 'CHECK'::text
WHEN 'f'::"char" THEN 'FOREIGN KEY'::text
WHEN 'p'::"char" THEN 'PRIMARY KEY'::text
WHEN 'u'::"char" THEN 'UNIQUE'::text
ELSE NULL::text
END::information_schema.character_data AS constraint_type,
CASE
WHEN c.condeferrable THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS is_deferrable,
CASE
WHEN c.condeferred THEN 'YES'::text
ELSE 'NO'::text
END::information_schema.yes_or_no AS initially_deferred
FROM pg_namespace nc,
pg_namespace nr,
pg_constraint c,
pg_class r
WHERE nc.oid = c.connamespace AND nr.oid = r.relnamespace AND c.conrelid = r.oid AND (c.contype <> ALL (ARRAY['t'::"char", 'x'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)
/*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/
UNION ALL
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
nr.nspname::information_schema.sql_identifier AS constraint_schema,
(((((nr.oid::text || '_'::text) || r.oid::text) || '_'::text) || a.attnum::text) || '_not_null'::text)::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
nr.nspname::information_schema.sql_identifier AS table_schema,
r.relname::information_schema.sql_identifier AS table_name,
'CHECK'::character varying::information_schema.character_data AS constraint_type,
'NO'::character varying::information_schema.yes_or_no AS is_deferrable,
'NO'::character varying::information_schema.yes_or_no AS initially_deferred
FROM pg_namespace nr,
pg_class r,
pg_attribute a
WHERE nr.oid = r.relnamespace AND r.oid = a.attrelid AND a.attnotnull AND a.attnum > 0 AND NOT a.attisdropped AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)
/*--AND (pg_has_role(r.relowner, 'USAGE'::text) OR has_table_privilege(r.oid, 'INSERT, UPDATE, DELETE, TRUNCATE, REFERENCES, TRIGGER'::text) OR has_any_column_privilege(r.oid, 'INSERT, UPDATE, REFERENCES'::text))*/
),
/*
-- CTE to replace information_schema.key_column_usage to remove owner limit
*/
kc AS (
SELECT current_database()::information_schema.sql_identifier AS constraint_catalog,
ss.nc_nspname::information_schema.sql_identifier AS constraint_schema,
ss.conname::information_schema.sql_identifier AS constraint_name,
current_database()::information_schema.sql_identifier AS table_catalog,
ss.nr_nspname::information_schema.sql_identifier AS table_schema,
ss.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
(ss.x).n::information_schema.cardinal_number AS ordinal_position,
CASE
WHEN ss.contype = 'f'::"char" THEN information_schema._pg_index_position(ss.conindid, ss.confkey[(ss.x).n])
ELSE NULL::integer
END::information_schema.cardinal_number AS position_in_unique_constraint
FROM pg_attribute a,
( SELECT r.oid AS roid,
r.relname,
r.relowner,
nc.nspname AS nc_nspname,
nr.nspname AS nr_nspname,
c.oid AS coid,
c.conname,
c.contype,
c.conindid,
c.confkey,
c.confrelid,
information_schema._pg_expandarray(c.conkey) AS x
FROM pg_namespace nr,
pg_class r,
pg_namespace nc,
pg_constraint c
WHERE nr.oid = r.relnamespace AND r.oid = c.conrelid AND nc.oid = c.connamespace AND (c.contype = ANY (ARRAY['p'::"char", 'u'::"char", 'f'::"char"])) AND r.relkind = 'r'::"char" AND NOT pg_is_other_temp_schema(nr.oid)) ss
WHERE ss.roid = a.attrelid AND a.attnum = (ss.x).x AND NOT a.attisdropped
/*--AND (pg_has_role(ss.relowner, 'USAGE'::text) OR has_column_privilege(ss.roid, a.attnum, 'SELECT, INSERT, UPDATE, REFERENCES'::text))*/
)
SELECT
kc.table_schema,
kc.table_name,
kc.column_name
FROM
/*
--information_schema.table_constraints tc,
--information_schema.key_column_usage kc
*/
tc, 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 NOT IN ('pg_catalog', 'information_schema') |]
pkFromRow :: [Table] -> (Schema, Text, Text) -> Maybe PrimaryKey
pkFromRow tabs (s, t, n) = PrimaryKey <$> table <*> pure n
where table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
allSynonyms :: [Column] -> H.Tx P.Postgres s [(Column,Column)]
allSynonyms allCols = do
syns <- H.listEx $ [H.stmt|
WITH synonyms AS (
/*
-- CTE to replace the view from information_schema because the information in it depended on the logged in role
-- notice the commented line
*/
WITH view_column_usage AS (
SELECT DISTINCT
CAST(current_database() AS character varying) AS view_catalog,
CAST(nv.nspname AS character varying) AS view_schema,
CAST(v.relname AS character varying) AS view_name,
CAST(current_database() AS character varying) AS table_catalog,
CAST(nt.nspname AS character varying) AS table_schema,
CAST(t.relname AS character varying) AS table_name,
CAST(a.attname AS character varying) AS column_name
FROM pg_namespace nv, pg_class v, pg_depend dv,
pg_depend dt, pg_class t, pg_namespace nt,
pg_attribute a
WHERE nv.oid = v.relnamespace
AND v.relkind = 'v'
AND v.oid = dv.refobjid
AND dv.refclassid = 'pg_catalog.pg_class'::regclass
AND dv.classid = 'pg_catalog.pg_rewrite'::regclass
AND dv.deptype = 'i'
AND dv.objid = dt.objid
AND dv.refobjid <> dt.refobjid
AND dt.classid = 'pg_catalog.pg_rewrite'::regclass
AND dt.refclassid = 'pg_catalog.pg_class'::regclass
AND dt.refobjid = t.oid
AND t.relnamespace = nt.oid
AND t.relkind IN ('r', 'v', 'f')
AND t.oid = a.attrelid
AND dt.refobjsubid = a.attnum
/*--AND pg_has_role(t.relowner, 'USAGE')*/
)
SELECT
vcu.table_schema AS src_table_schema,
vcu.table_name AS src_table_name,
vcu.column_name AS src_column_name,
view.schemaname AS syn_table_schema,
view.viewname AS syn_table_name,
view.definition AS view_definition
FROM
pg_catalog.pg_views AS view,
view_column_usage AS vcu
WHERE
view.schemaname = vcu.view_schema AND
view.viewname = vcu.view_name AND
view.schemaname NOT IN ('pg_catalog', 'information_schema')
/*--AND (SELECT COUNT(*) FROM information_schema.view_table_usage WHERE view_schema = view.schemaname AND view_name = view.viewname) = 1*/
allSynonyms :: [Column] -> H.Query () [(Column,Column)]
allSynonyms cols =
H.statement sql HE.unit (decodeSynonyms cols) True
where
-- query explanation at https://gist.github.com/ruslantalpa/2eab8c930a65e8043d8f
sql = [q|
WITH view_columns AS (
SELECT
c.oid AS view_oid,
a.attname::information_schema.sql_identifier AS column_name
FROM pg_attribute a
JOIN pg_class c ON a.attrelid = c.oid
JOIN pg_namespace nc ON c.relnamespace = nc.oid
WHERE
NOT pg_is_other_temp_schema(nc.oid)
AND a.attnum > 0
AND NOT a.attisdropped
AND (c.relkind = 'v'::"char")
AND nc.nspname NOT IN ('information_schema', 'pg_catalog')
),
view_column_usage AS (
SELECT DISTINCT
v.oid as view_oid,
nv.nspname::information_schema.sql_identifier AS view_schema,
v.relname::information_schema.sql_identifier AS view_name,
nt.nspname::information_schema.sql_identifier AS table_schema,
t.relname::information_schema.sql_identifier AS table_name,
a.attname::information_schema.sql_identifier AS column_name,
pg_get_viewdef(v.oid)::information_schema.character_data AS view_definition
FROM pg_namespace nv
JOIN pg_class v ON nv.oid = v.relnamespace
JOIN pg_depend dv ON v.oid = dv.refobjid
JOIN pg_depend dt ON dv.objid = dt.objid
JOIN pg_class t ON dt.refobjid = t.oid
JOIN pg_namespace nt ON t.relnamespace = nt.oid
JOIN pg_attribute a ON t.oid = a.attrelid AND dt.refobjsubid = a.attnum
WHERE
nv.nspname not in ('information_schema', 'pg_catalog')
AND v.relkind = 'v'::"char"
AND dv.refclassid = 'pg_class'::regclass::oid
AND dv.classid = 'pg_rewrite'::regclass::oid
AND dv.deptype = 'i'::"char"
AND dv.refobjid <> dt.refobjid
AND dt.classid = 'pg_rewrite'::regclass::oid
AND dt.refclassid = 'pg_class'::regclass::oid
AND (t.relkind = ANY (ARRAY['r'::"char", 'v'::"char", 'f'::"char"]))
),
candidates AS (
SELECT
vcu.*,
(
SELECT CASE WHEN match IS NOT NULL THEN coalesce(match[7], match[4]) END
FROM REGEXP_MATCHES(
CONCAT('SELECT ', SPLIT_PART(vcu.view_definition, 'SELECT', 2)),
CONCAT('SELECT.*?((',vcu.table_name,')|(\w+))\.(', vcu.column_name, ')(\sAS\s(")?([^"]+)\6)?.*?FROM.*?',vcu.table_schema,'\.(\2|',vcu.table_name,'\s+(AS\s)?\3)'),
'ns'
) match
) AS view_column_name
FROM view_column_usage AS vcu
)
SELECT
src_table_schema, src_table_name, src_column_name,
syn_table_schema, syn_table_name,
(regexp_matches(view_definition, CONCAT('\.(', src_column_name, ')(?=,|$)'), 'gn'))[1] AS syn_column_name
FROM synonyms
UNION (
SELECT
src_table_schema, src_table_name, src_column_name,
syn_table_schema, syn_table_name,
(regexp_matches(view_definition, CONCAT('\.', src_column_name, '\sAS\s("?)(.+?)\1(,|$)'), 'gn'))[2] AS syn_column_name /* " <- for syntax highlighting */
FROM synonyms
)
c.table_schema,
c.table_name,
c.column_name AS table_column_name,
c.view_schema,
c.view_name,
c.view_column_name
FROM view_columns AS vc, candidates AS c
WHERE
vc.view_oid = c.view_oid AND
vc.column_name = c.view_column_name
ORDER BY c.view_schema, c.view_name, c.table_name, c.view_column_name
|]
return $ mapMaybe (synonymFromRow allCols) syns
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
+47 -30
View File
@@ -2,58 +2,75 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
module PostgREST.Error (PgError, pgErrResponse, errResponse) where
module PostgREST.Error (pgErrResponse, errResponse) where
import Data.Aeson ((.=))
import qualified Data.Aeson as JSON
import Data.Maybe (fromMaybe)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.String.Utils (replace)
import Data.Text (Text)
import qualified Data.Text as T
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Hasql.Pool as P
import qualified Hasql.Session as H
import Network.HTTP.Types.Header
import qualified Network.HTTP.Types.Status as HT
import Network.Wai (Response, responseLBS)
type PgError = H.SessionError P.Postgres
errResponse :: HT.Status -> Text -> Response
errResponse status message = responseLBS status [(hContentType, "application/json")] (cs $ T.concat ["{\"message\":\"",message,"\"}"])
pgErrResponse :: PgError -> Response
pgErrResponse :: P.UsageError -> Response
pgErrResponse 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 [
instance JSON.ToJSON P.UsageError where
toJSON (P.ConnectionError e) = JSON.object [
"code" .= ("" :: T.Text),
"message" .= ("Connection error" :: T.Text),
"details" .= (cs (fromMaybe "" e) :: T.Text)]
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
instance JSON.ToJSON H.Error where
toJSON (H.ResultError (H.ServerError 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),
toJSON (H.ResultError (H.UnexpectedResult m)) = JSON.object [
"message" .= (cs m::T.Text)]
toJSON (H.ResultError (H.RowError i H.EndOfInput)) = JSON.object [
"message" .= ("Row error: end of input"::String),
"details" .=
("Attempt to parse more columns than there are in the result"::String),
"details" .= ("Row number " <> show i)]
toJSON (H.ResultError (H.RowError i H.UnexpectedNull)) = JSON.object [
"message" .= ("Row error: unexpected null"::String),
"details" .= ("Attempt to parse a NULL as some value."::String),
"details" .= ("Row number " <> show i)]
toJSON (H.ResultError (H.RowError i (H.ValueError d))) = JSON.object [
"message" .= ("Row error: Wrong value parser used"::String),
"details" .= d,
"details" .= ("Row number " <> show i)]
toJSON (H.ResultError (H.UnexpectedAmountOfRows i)) = JSON.object [
"message" .= ("Unexpected amount of rows"::String),
"details" .= i]
toJSON (H.ClientError d) = JSON.object [
"message" .= ("Database client error"::String),
"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
httpStatus :: P.UsageError -> HT.Status
httpStatus (P.ConnectionError _) =
HT.status500
httpStatus (P.SessionError (H.ResultError (H.ServerError c _ _ _))) =
case cs c 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
"23503" -> HT.status409 -- foreign_key_violation
"23505" -> HT.status409 -- unique_violation
'2':'5':_ -> HT.status500 -- invalid tx state
'2':'8':_ -> HT.status403 -- invalid auth specification
'2':'D':_ -> HT.status500 -- invalid tx termination
@@ -70,8 +87,8 @@ httpStatus (H.TxError (P.ErroneousResult codeBS _ _ _)) =
'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
"42P01" -> HT.status404 -- undefined table
"42501" -> HT.status404 -- insufficient privilege
_ -> HT.status400
httpStatus (P.SessionError (H.ResultError _)) = HT.status500
httpStatus (P.SessionError (H.ClientError _)) = HT.status503
+32 -51
View File
@@ -9,40 +9,34 @@ import PostgREST.Config (AppConfig (..),
prettyVersion,
readOptions)
import PostgREST.DbStructure
import PostgREST.Error (PgError, pgErrResponse)
import PostgREST.Middleware
import Control.Monad (unless, void)
import Control.Monad.IO.Class (liftIO)
import Data.Aeson (encode)
import Data.Functor.Identity
import Control.Monad
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Time.Clock.POSIX (getPOSIXTime)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Network.Wai
import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.RequestLogger (logStdout)
import qualified Hasql.Query as H
import qualified Hasql.Session as H
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Pool as P
import Network.Wai.Handler.Warp
import System.IO (BufferMode (..),
hSetBuffering, stderr,
stdin, stdout)
import Web.JWT (secret)
#ifndef mingw32_HOST_OS
import System.Posix.Signals
import Control.Concurrent (myThreadId)
import Control.Exception.Base (throwTo, AsyncException(..))
#endif
isServerVersionSupported :: H.Session P.Postgres IO Bool
isServerVersionSupported :: H.Session Bool
isServerVersionSupported = do
Identity (row :: Text) <- H.tx Nothing $ H.singleEx [H.stmt|SHOW server_version_num|]
return $ read (cs row) >= minimumPgVersion
hasqlError :: PgError -> IO a
hasqlError = error . cs . encode
ver <- H.query () pgVersion
return $ read (cs ver) >= minimumPgVersion
where
pgVersion =
H.statement "SHOW server_version_num"
HE.unit (HD.singleRow $ HD.value HD.text) True
main :: IO ()
main = do
@@ -52,46 +46,33 @@ main = do
conf <- readOptions
let port = configPort conf
pgSettings = cs (configDatabase conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
unless (secret "secret" /= configJwtSecret conf) $
putStrLn "WARNING, running in insecure mode, JWT secret is the default value"
Prelude.putStrLn $ "Listening on port " ++
(show $ configPort conf :: String)
let pgSettings = P.StringSettings $ cs (configDatabase conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle = logStdout . defaultMiddle
poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres <- H.acquirePool pgSettings poolSettings
supportedOrError <- H.session pool isServerVersionSupported
either hasqlError
(\supported ->
unless supported $
error (
"Cannot run in this PostgreSQL version, PostgREST needs at least "
<> show minimumPgVersion)
) supportedOrError
pool <- P.acquire (configPool conf, 10, pgSettings)
#ifndef mingw32_HOST_OS
tid <- myThreadId
void $ installHandler keyboardSignal (Catch $ do
H.releasePool pool
throwTo tid UserInterrupt
) Nothing
forM_ [sigINT, sigTERM] $ \sig ->
void $ installHandler sig (Catch $ do
P.release pool
throwTo tid UserInterrupt
) Nothing
#endif
let txSettings = Just (H.ReadCommitted, Just True)
dbOrError <- H.session pool $ H.tx txSettings $ getDbStructure (cs $ configSchema conf)
dbStructure <- either hasqlError return dbOrError
result <- P.use pool $ do
supported <- isServerVersionSupported
unless supported $ error (
"Cannot run in this PostgreSQL version, PostgREST needs at least "
<> show minimumPgVersion)
getDbStructure (cs $ configSchema conf)
runSettings appSettings $ middle $ \ req respond -> do
time <- getPOSIXTime
body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx txSettings $
runWithClaims conf time (app dbStructure conf body) req
either (respond . pgErrResponse) respond resOrError
let dbStructure = either (error.show) id result
runSettings appSettings $ postgrest conf dbStructure pool
+28 -29
View File
@@ -3,56 +3,55 @@
module PostgREST.Middleware where
import Control.Monad (unless)
import qualified Data.ByteString as BS
import qualified Data.HashMap.Strict as M
import Data.Maybe (fromMaybe)
import Data.Text
import Data.String.Conversions (cs)
import Data.Text
import Data.Time.Clock (NominalDiffTime)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Hasql.Transaction as H
import Network.HTTP.Types.Header (hAccept, hAuthorization)
import Network.HTTP.Types.Status (status415, status400)
import Network.Wai (Application, Request (..), Response,
requestHeaders)
import Network.HTTP.Types.Status (status400, status415)
import Network.Wai (Application, Request (..),
Response, requestHeaders)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy)
import PostgREST.ApiRequest (pickContentType)
import PostgREST.ApiRequest (pickContentType)
import PostgREST.Auth (setRole, jwtClaims, claimsToSQL)
import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (errResponse)
import Prelude hiding(concat)
import Prelude hiding (concat, null)
import qualified Data.Vector as V
import qualified Hasql.Backend as B
import qualified Data.Map.Lazy as M
runWithClaims :: forall s. AppConfig -> NominalDiffTime ->
(Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response
runWithClaims :: AppConfig -> NominalDiffTime ->
(Request -> H.Transaction Response) ->
Request -> H.Transaction Response
runWithClaims conf time app req = do
_ <- H.unitEx $ stmt setAnon
case split (== ' ') (cs auth) of
("Bearer" : tokenStr : _) ->
case jwtClaims jwtSecret tokenStr time of
Just claims ->
if M.member "role" claims
then do
mapM_ H.unitEx $ stmt <$> claimsToSQL claims
app req
else invalidJWT
_ -> invalidJWT
_ -> app req
H.sql setAnon
let tokenStr = case split (== ' ') (cs auth) of
("Bearer" : t : _) -> t
_ -> ""
eClaims = jwtClaims jwtSecret tokenStr time
case eClaims of
Left e -> clientErr e
Right claims ->
if M.null claims && not (null tokenStr)
then clientErr "Invalid JWT"
else do
let cmdBatch = mconcat $ claimsToSQL claims
unless (BS.null cmdBatch) (H.sql cmdBatch)
app req
where
stmt c = B.Stmt c V.empty True
hdrs = requestHeaders req
jwtSecret = configJwtSecret conf
auth = fromMaybe "" $ lookup hAuthorization hdrs
anon = cs $ configAnonRole conf
setAnon = setRole anon
invalidJWT = return $ errResponse status400 "Invalid JWT"
clientErr = return . errResponse status400
unsupportedAccept :: Application -> Application
unsupportedAccept app req respond =
+2 -2
View File
@@ -3,14 +3,14 @@ module PostgREST.Parsers
-- )
where
import Control.Applicative hiding ((<$>))
import Control.Applicative hiding ((<$>))
import Data.Monoid
import Data.String.Conversions (cs)
import Data.Text (Text)
import Data.Tree
import PostgREST.QueryBuilder (operators)
import PostgREST.Types
import Text.ParserCombinators.Parsec hiding (many, (<|>))
import PostgREST.QueryBuilder (operators)
pRequestSelect :: Text -> Parser ReadRequest
pRequestSelect rootNodeName = do
+137 -88
View File
@@ -15,7 +15,6 @@ Any function that outputs a SQL fragment should be in this module.
module PostgREST.QueryBuilder (
addRelations
, addJoinConditions
, asJson
, callProc
, createReadStatement
, createWriteStatement
@@ -26,28 +25,33 @@ module PostgREST.QueryBuilder (
, requestToCountQuery
, sourceCTEName
, unquoted
, ResultsWithCount
) where
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import qualified Hasql.Query as H
import qualified Hasql.Encoders as HE
import qualified Hasql.Decoders as HD
import qualified Data.Aeson as JSON
import Data.Int (Int64)
import PostgREST.RangeQuery (NonnegRange, rangeLimit, rangeOffset)
import Control.Error (note, fromMaybe, mapMaybe)
import Data.Functor.Contravariant (contramap)
import qualified Data.HashMap.Strict as HM
import Data.List (find, (\\))
import Data.Monoid ((<>))
import Data.Text (Text, intercalate, unwords, replace, isInfixOf, toLower, split)
import qualified Data.Text as T (map, takeWhile)
import qualified Data.Text.Encoding as T
import Data.String.Conversions (cs)
import Control.Applicative (empty, (<|>))
import Control.Applicative ((<|>))
import Control.Monad (join)
import Data.Tree (Tree(..))
import qualified Data.Vector as V
import PostgREST.Types
import qualified Data.Map as M
import Text.InterpolatedString.Perl6 (qc)
import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS
import Data.Scientific ( FPFormat (..)
@@ -57,70 +61,101 @@ import Data.Scientific ( FPFormat (..)
import Prelude hiding (unwords)
import PostgREST.ApiRequest (PreferRepresentation (..))
type PStmt = H.Stmt P.Postgres
instance Monoid PStmt where
mappend (B.Stmt query params prep) (B.Stmt query' params' prep') =
B.Stmt (query <> query') (params <> params') (prep && prep')
mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt
createReadStatement :: SqlQuery -> SqlQuery -> NonnegRange -> Bool -> Bool -> Bool -> B.Stmt P.Postgres
{-| The generic query result format used by API responses -}
type ResultsWithCount = (Maybe Int64, Int64, BS.ByteString, BS.ByteString)
{-| Read and Write api requests use a similar response format which includes
various record counts and possible location header. This is the decoder
for that common type of query.
-}
decodeStandard :: HD.Result ResultsWithCount
decodeStandard =
HD.singleRow standardRow
where
standardRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
<*> HD.value HD.bytea <*> HD.value HD.bytea
decodeStandardMay :: HD.Result (Maybe ResultsWithCount)
decodeStandardMay =
HD.maybeRow standardRow
where
standardRow = (,,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
<*> HD.value HD.bytea <*> HD.value HD.bytea
{-| JSON and CSV payloads from the client are given to us as
UniformObjects (objects who all have the same keys),
and we turn this into an old fasioned JSON array
-}
encodeUniformObjs :: HE.Params UniformObjects
encodeUniformObjs =
contramap (JSON.Array . V.map JSON.Object . unUniformObjects) (HE.value HE.json)
createReadStatement :: SqlQuery -> SqlQuery -> NonnegRange -> Bool -> Bool -> Bool ->
H.Query () ResultsWithCount
createReadStatement selectQuery countQuery range isSingle countTotal asCsv =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> selectQuery <> ") " <>
"SELECT " <> intercalate ", " [
unicodeStatement sql HE.unit decodeStandard True
where
sql = [qc|
WITH {sourceCTEName} AS ({selectQuery}) SELECT {cols}
FROM ( SELECT * FROM {sourceCTEName} {limitF range}) t |]
countResultF = if countTotal then "("<>countQuery<>")" else "null"
cols = intercalate ", " [
countResultF <> " AS total_result_set",
"pg_catalog.count(t) AS page_total",
"null AS header",
"'' AS header",
bodyF <> " AS body"
] <>
" FROM ( SELECT * FROM " <> sourceCTEName <> " " <> limitF range <> ") t"
) V.empty True
where
countResultF = if countTotal then "("<>countQuery<>")" else "null"
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
]
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
createWriteStatement :: QualifiedIdentifier -> SqlQuery -> SqlQuery -> Bool -> PreferRepresentation ->
[Text] -> Bool -> Payload -> B.Stmt P.Postgres
createWriteStatement :: QualifiedIdentifier -> SqlQuery -> SqlQuery -> Bool ->
PreferRepresentation -> [Text] -> Bool -> Payload ->
H.Query UniformObjects (Maybe ResultsWithCount)
createWriteStatement _ _ _ _ _ _ _ (PayloadParseError _) = undefined
createWriteStatement _ _ mutateQuery _ None
_ _ (PayloadJSON (UniformObjects rows)) =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> mutateQuery <> ") " <>
"SELECT null, 0, null, null"
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
_ _ (PayloadJSON (UniformObjects _)) =
unicodeStatement sql encodeUniformObjs decodeStandardMay True
where
sql = [qc|
WITH {sourceCTEName} AS ({mutateQuery})
SELECT '', 0, '', '' |]
createWriteStatement qi _ mutateQuery isSingle HeadersOnly
pKeys _ (PayloadJSON (UniformObjects rows)) =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> mutateQuery <> " RETURNING " <> fromQi qi <> ".*" <> ") " <>
"SELECT " <> intercalate ", " [
"null AS total_result_set",
pKeys _ (PayloadJSON (UniformObjects _)) =
unicodeStatement sql encodeUniformObjs decodeStandardMay True
where
sql = [qc|
WITH {sourceCTEName} AS ({mutateQuery} RETURNING {fromQi qi}.*)
SELECT {cols}
FROM (SELECT 1 FROM {sourceCTEName}) t |]
cols = intercalate ", " [
"'' AS total_result_set",
"pg_catalog.count(t) AS page_total",
if isSingle then locationF pKeys else "null",
"null"
] <>
" FROM (SELECT 1 FROM " <> sourceCTEName <> ") t"
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
if isSingle then locationF pKeys else "''",
"''"
]
createWriteStatement qi selectQuery mutateQuery isSingle Full
pKeys asCsv (PayloadJSON (UniformObjects rows)) =
B.Stmt (
"WITH " <> sourceCTEName <> " AS (" <> mutateQuery <> " RETURNING " <> fromQi qi <> ".*" <> ") " <>
"SELECT " <> intercalate ", " [
"null AS total_result_set", -- when updateing it does not make sense
pKeys asCsv (PayloadJSON (UniformObjects _)) =
unicodeStatement sql encodeUniformObjs decodeStandardMay True
where
sql = [qc|
WITH {sourceCTEName} AS ({mutateQuery} RETURNING {fromQi qi}.*)
SELECT {cols}
FROM ({selectQuery}) t |]
cols = intercalate ", " [
"'' AS total_result_set", -- when updateing it does not make sense
"pg_catalog.count(t) AS page_total",
if isSingle then locationF pKeys else "null" <> " AS header",
if isSingle then locationF pKeys else "''" <> " AS header",
bodyF <> " AS body"
] <>
" FROM ( "<>selectQuery<>") t"
) (V.singleton . B.encodeValue . JSON.Array . V.map JSON.Object $ rows) True
where
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
]
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either Text ReadRequest
addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _)) forest) =
@@ -131,8 +166,8 @@ addRelations schema allRelations parentNode node@(Node readNode@(query, (name, _
$ findRelationByTable schema name parentTable
<|> findRelationByColumn schema parentTable name
addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation))
addRel (q, (n, _)) r = (q {from=fromRelation}, (n, Just r))
where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from q)
addRel (query', (n, _)) r = (query' {from=fromRelation}, (n, Just r))
where fromRelation = map (\t -> if t == n then tableName (relTable r) else t) (from query')
_ -> Node (query, (name, Nothing)) <$> updatedForest
where
@@ -150,13 +185,13 @@ addJoinConditions :: Schema -> ReadRequest -> Either Text ReadRequest
addJoinConditions schema (Node (query, (n, r)) forest) =
case r of
Nothing -> Node (updatedQuery, (n,r)) <$> updatedForest -- this is the root node
Just rel@(Relation{relType=Child}) -> Node (addCond updatedQuery (getJoinConditions rel),(n,r)) <$> updatedForest
Just (Relation{relType=Parent}) -> Node (updatedQuery, (n,r)) <$> updatedForest
Just rel@(Relation{relType=Many, relLTable=(Just linkTable)}) ->
Just rel@Relation{relType=Child} -> Node (addCond updatedQuery (getJoinConditions rel),(n,r)) <$> updatedForest
Just Relation{relType=Parent} -> Node (updatedQuery, (n,r)) <$> updatedForest
Just rel@Relation{relType=Many, relLTable=(Just linkTable)} ->
Node (qq, (n, r)) <$> updatedForest
where
q = addCond updatedQuery (getJoinConditions rel)
qq = q{from=tableName linkTable : from q}
query' = addCond updatedQuery (getJoinConditions rel)
qq = query'{from=tableName linkTable : from query'}
_ -> Left "unknown relation"
where
-- add parentTable and parentJoinConditions to the query
@@ -164,23 +199,35 @@ addJoinConditions schema (Node (query, (n, r)) forest) =
where
parentJoinConditions = map (getJoinConditions . snd) parents
parents = mapMaybe (getParents . rootLabel) forest
getParents (_, (tbl, Just rel@(Relation{relType=Parent}))) = Just (tbl, rel)
getParents (_, (tbl, Just rel@Relation{relType=Parent})) = Just (tbl, rel)
getParents _ = Nothing
updatedForest = mapM (addJoinConditions schema) forest
addCond q con = q{flt_=con ++ flt_ q}
addCond query' con = query'{flt_=con ++ flt_ query'}
asJson :: StatementT
asJson s = s {
B.stmtTemplate =
"array_to_json(coalesce(array_agg(row_to_json(t)), '{}'))::character varying from ("
<> B.stmtTemplate s <> ") t" }
callProc :: QualifiedIdentifier -> JSON.Object -> PStmt
callProc qi params = do
let args = intercalate "," $ map assignment (HM.toList params)
B.Stmt ("select * from " <> fromQi qi <> "(" <> args <> ")") empty True
type ProcResults = (Maybe Int64, Int64, JSON.Value)
callProc :: QualifiedIdentifier -> JSON.Object -> NonnegRange -> Bool -> H.Query () (Maybe ProcResults)
callProc qi params range countTotal =
unicodeStatement sql HE.unit decodeProc True
where
assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
sql = [qc|
WITH t AS (select * {_callSql})
SELECT
{_countExpr} as countTotal,
pg_catalog.count(1) as countResult,
array_to_json(
coalesce(array_agg(row_to_json(r)), '\{}')
)::character varying
FROM (select * from t {limitF range}) r;
|]
_args = intercalate "," $ map _assignment (HM.toList params)
_assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
_callSql = [qc| from {fromQi qi}({_args}) |] :: Text
_countExpr = if countTotal
then "(select pg_catalog.count(1) from t)"
else "null::bigint" :: Text
decodeProc = HD.maybeRow procRow
procRow = (,,) <$> HD.nullableValue HD.int8 <*> HD.value HD.int8
<*> HD.value HD.json
operators :: [(Text, SqlFragment)]
operators = [
@@ -209,7 +256,7 @@ pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> replace "'" "''" trimmed <> "'"
slashed = replace "\\" "\\\\" escaped in
if "\\\\" `isInfixOf` escaped
if "\\" `isInfixOf` escaped
then "E" <> slashed
else slashed
@@ -222,8 +269,8 @@ requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _))
("WHERE " <> intercalate " AND " ( map (pgFmtCondition (QualifiedIdentifier schema mainTbl)) localConditions )) `emptyOnNull` localConditions
]
where
fn (Filter{value=VText _}) = True
fn (Filter{value=VForeignKey _ _}) = False
fn Filter{value=VText _} = True
fn Filter{value=VForeignKey _ _} = False
localConditions = filter fn conditions
requestToQuery :: Schema -> DbRequest -> SqlQuery
@@ -268,19 +315,19 @@ requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nod
filterParentConditions parentTable (Filter _ _ (VForeignKey (QualifiedIdentifier "" t) _)) = parentTable == t
filterParentConditions _ _ = False
getQueryParts :: Tree ReadNode -> ([(SqlFragment, TableName)], [SqlFragment]) -> ([(SqlFragment,TableName)], [SqlFragment])
getQueryParts (Node n@(_, (name, Just (Relation {relType=Child,relTable=Table{tableName=table}}))) forst) (j,s) = (j,sel:s)
getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}})) forst) (j,s) = (j,sel:s)
where
sel = "COALESCE(("
<> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent name
where subquery = requestToQuery schema (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just (Relation {relType=Parent,relTable=Table{tableName=table}}))) forst) (j,s) = (joi:j,sel:s)
getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}})) forst) (j,s) = (joi:j,sel:s)
where
sel = "row_to_json(" <> pgFmtIdent table <> ".*) AS "<>pgFmtIdent name --TODO must be singular
joi = ("( " <> subquery <> " ) AS " <> pgFmtIdent table, table)
where subquery = requestToQuery schema (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just (Relation {relType=Many,relTable=Table{tableName=table}}))) forst) (j,s) = (j,sel:s)
getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}})) forst) (j,s) = (j,sel:s)
where
sel = "COALESCE (("
<> "SELECT array_to_json(array_agg(row_to_json("<>pgFmtIdent table<>"))) "
@@ -299,7 +346,7 @@ requestToQuery schema (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects row
"INSERT INTO ", fromQi qi,
" (" <> colsString <> ")" <>
" SELECT " <> colsString <>
" FROM json_populate_recordset(null::" , fromQi qi, ", ?)"
" FROM json_populate_recordset(null::" , fromQi qi, ", $1)"
]
requestToQuery schema (DbMutate (Update mainTbl (PayloadJSON (UniformObjects rows)) conditions)) =
case rows V.!? 0 of
@@ -339,7 +386,7 @@ asCsvF :: SqlFragment
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
where
asCsvHeaderF =
"(SELECT string_agg(a.k, ',')" <>
"(SELECT coalesce(string_agg(a.k, ','), '')" <>
" FROM (" <>
" SELECT json_object_keys(r)::TEXT as k" <>
" FROM ( " <>
@@ -350,10 +397,10 @@ asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
asCsvBodyF = "coalesce(string_agg(substring(t::text, 2, length(t::text) - 2), '\n'), '')"
asJsonF :: SqlFragment
asJsonF = "array_to_json(array_agg(row_to_json(t)))::character varying"
asJsonF = "coalesce(array_to_json(array_agg(row_to_json(t))), '[]')::character varying"
asJsonSingleF :: SqlFragment --TODO! unsafe when the query actually returns multiple rows, used only on inserting and returning single element
asJsonSingleF = "string_agg(row_to_json(t)::text, ',')::character varying "
asJsonSingleF = "coalesce(string_agg(row_to_json(t)::text, ','), '')::character varying "
locationF :: [Text] -> SqlFragment
locationF pKeys =
@@ -365,8 +412,7 @@ locationF pKeys =
if null pKeys
then ""
else " WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')"
) <>
")"
) <> ")"
limitF :: NonnegRange -> SqlFragment
limitF r = "LIMIT " <> limit <> " OFFSET " <> offset
@@ -394,6 +440,9 @@ getJoinConditions (Relation t cols ft fcs typ lt lc1 lc2) =
toFilter :: Text -> Text -> Column -> Column -> Filter
toFilter tb ftb c fc = Filter (colName c, Nothing) "=" (VForeignKey (QualifiedIdentifier s tb) (ForeignKey fc{colTable=(colTable fc){tableName=ftb}}))
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Query a b
unicodeStatement = H.statement . T.encodeUtf8
emptyOnNull :: Text -> [a] -> Text
emptyOnNull val x = if null x then "" else val
+6 -6
View File
@@ -24,7 +24,7 @@ import Data.Maybe (fromMaybe, listToMaybe)
import Prelude
type NonnegRange = Range Int
type NonnegRange = Range Integer
rangeParse :: BS.ByteString -> NonnegRange
rangeParse range = do
@@ -41,28 +41,28 @@ rangeParse range = do
rangeRequested :: RequestHeaders -> NonnegRange
rangeRequested = rangeParse . fromMaybe "" . lookup hRange
restrictRange :: Maybe Int -> NonnegRange -> NonnegRange
restrictRange :: Maybe Integer -> NonnegRange -> NonnegRange
restrictRange Nothing r = r
restrictRange (Just limit) r =
rangeIntersection r $
Range BoundaryBelowAll (BoundaryAbove $ rangeOffset r + limit - 1)
rangeLimit :: NonnegRange -> Maybe Int
rangeLimit :: NonnegRange -> Maybe Integer
rangeLimit range =
case [rangeLower range, rangeUpper range] of
[BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
rangeOffset :: NonnegRange -> Int
rangeOffset :: NonnegRange -> Integer
rangeOffset range =
case rangeLower range of
BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
rangeGeq :: Int -> NonnegRange
rangeGeq :: Integer -> NonnegRange
rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll
rangeLeq :: Int -> NonnegRange
rangeLeq :: Integer -> NonnegRange
rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n)
+14 -10
View File
@@ -1,15 +1,16 @@
module PostgREST.Types where
import Data.Text
import Data.Tree
import qualified Data.ByteString.Lazy as BL
import Data.Aeson
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import Data.Int (Int32)
import Data.Text
import Data.Tree
import qualified Data.Vector as V
import Data.Aeson
data DbStructure = DbStructure {
dbTables :: [Table]
, dbColumns :: [Column]
, dbRelations :: [Relation]
dbTables :: [Table]
, dbColumns :: [Column]
, dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey]
} deriving (Show, Eq)
@@ -31,12 +32,12 @@ data Column =
Column {
colTable :: Table
, colName :: Text
, colPosition :: Int
, colPosition :: Int32
, colNullable :: Bool
, colType :: Text
, colUpdatable :: Bool
, colMaxLen :: Maybe Int
, colPrecision :: Maybe Int
, colMaxLen :: Maybe Int32
, colPrecision :: Maybe Int32
, colDefault :: Maybe Text
, colEnum :: [Text]
, colFK :: Maybe ForeignKey
@@ -90,6 +91,9 @@ data Relation = Relation {
newtype UniformObjects = UniformObjects (V.Vector Object)
deriving (Show, Eq)
unUniformObjects :: UniformObjects -> V.Vector Object
unUniformObjects (UniformObjects objs) = objs
-- | When Hasql supports the COPY command then we can
-- have a special payload just for CSV, but until
-- then CSV is converted to a JSON array.
+13 -5
View File
@@ -1,7 +1,15 @@
flags: {}
packages:
- '.'
resolver: lts-5.5
extra-deps:
- Ranged-sets-0.3.0
- packdeps-0.4.1
resolver: nightly-2015-10-27
- bytestring-tree-builder-0.2.5
- hasql-0.19.9
- hasql-pool-0.4
- hasql-transaction-0.4.3
- packdeps-0.4.2.1
- postgresql-error-codes-1
- postgresql-binary-0.8.1
ghc-options:
postgrest: -O2 -Werror -Wall -fwarn-monomorphism-restriction -fwarn-missing-exported-sigs -fwarn-identities
packages:
- '.'
+20 -8
View File
@@ -6,16 +6,12 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.Wai (Application)
-- }}}
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool)
$ describe "authorization" $ do
spec :: SpecWith Application
spec = describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
@@ -25,9 +21,25 @@ spec struct pool = around (withApp cfgDefault struct pool)
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
, matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json"]
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
it "sql functions can encode custom and standard claims" $
post "/rpc/jwt_test" "{}"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6MTMwMDgxOTM4MCwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3QiLCJpYXQiOjEzMDA4MTkzODAsImF1ZCI6ImV2ZXJ5b25lIn0._tQCF79-ZZGMlLktd3csM_bVaiMg7A8YvIb6K2hcu5w"} |]
, matchStatus = 200
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
it "sql functions can read custom and standard claims variables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwLCJhdWQiOiJldmVyeW9uZSJ9.AQmCA7CMScvfaDRMqRPeUY6eNf--69gpW-kxaWfq9X0"
request methodPost "/rpc/reveal_big_jwt" [auth] "{}"
`shouldRespondWith` [json| [
{"sub":"fun", "jti":"foo", "nbf":1300819380, "exp":9999999999,
"http://postgrest.com/foo":true, "iss":"joe", "iat":1300819380,
"aud":"everyone"}] |]
it "allows users with permissions to see their tables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
+51
View File
@@ -0,0 +1,51 @@
{-# LANGUAGE MultiParamTypeClasses, TypeFamilies, UndecidableInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Feature.ConcurrentSpec where
import Control.Monad (void)
import Control.Monad.Base
import Control.Monad.Trans.Control
import Control.Concurrent.Async (mapConcurrently)
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai.Internal
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai.Test (Session)
import Network.Wai (Application)
spec :: SpecWith Application
spec =
describe "Queryiny in parallel" $
it "should not raise 'transaction in progress' error" $
raceTest 10 $
get "/fakefake"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json|
{ "hint": null,
"details":null,
"code":"42P01",
"message":"relation \"test.fakefake\" does not exist"
} |]
, matchStatus = 404
, matchHeaders = []
}
raceTest :: Int -> WaiExpectation -> WaiExpectation
raceTest times = liftBaseDiscard go
where
go test = void $ mapConcurrently (const test) [1..times]
instance MonadBaseControl IO WaiSession where
type StM WaiSession a = StM Session a
liftBaseWith f = WaiSession $
liftBaseWith $ \runInBase ->
f $ \k -> runInBase (unWaiSession k)
restoreM = WaiSession . restoreM
{-# INLINE liftBaseWith #-}
{-# INLINE restoreM #-}
instance MonadBase IO WaiSession where
liftBase = liftIO
+4 -6
View File
@@ -6,17 +6,15 @@ import Test.Hspec.Wai
import Network.Wai.Test (SResponse(simpleHeaders, simpleBody))
import qualified Data.ByteString.Lazy as BL
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types
import Network.Wai (Application)
-- }}}
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool) $ describe "CORS" $ do
spec :: SpecWith Application
spec =
describe "CORS" $ do
let preflightHeaders = [
("Accept", "*/*"),
("Origin", "http://example.com"),
+3 -9
View File
@@ -4,17 +4,11 @@ import Test.Hspec
import Test.Hspec.Wai
import Text.Heredoc
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types
import Network.Wai (Application)
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = beforeAll resetDb
. around (withApp cfgDefault struct pool) $
spec :: SpecWith Application
spec =
describe "Deleting" $ do
context "existing record" $ do
it "succeeds with 204 and deletion count" $
+62 -51
View File
@@ -5,25 +5,23 @@ import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import qualified Data.Aeson as JSON
import Data.Maybe (fromJust)
import Data.Monoid ((<>))
import Text.Heredoc
import Network.HTTP.Types.Header
import Network.HTTP.Types
import Control.Monad (replicateM_)
import Control.Monad (replicateM_, void)
import TestTypes(IncPK(..), CompoundPK(..))
import Network.Wai (Application)
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool) $ do
spec :: SpecWith Application
spec = do
describe "Posting new record" $ do
context "disparate csv types" $ do
context "disparate json types" $ do
it "accepts disparate json types" $ do
p <- post "/menagerie"
[json| {
@@ -44,7 +42,7 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
} |] `shouldRespondWith` ResponseMatcher {
matchBody = Just [str|{"integer":14,"varchar":"testing!"}|]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json"]
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
it "includes related data after insert" $
@@ -52,7 +50,7 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` ResponseMatcher {
matchBody = Just [str|{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}|]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json", "Location" <:> "/projects?id=eq.6"]
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8", "Location" <:> "/projects?id=eq.6"]
}
@@ -124,6 +122,18 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2" `shouldRespondWith` 400
context "with valid json payload" $
it "succeeds and returns 201 created" $
post "/simple_pk" [json| { "k":"k1", "extra":"e1" } |] `shouldRespondWith` 201
context "attempting to insert a row with the same primary key" $
it "fails returning a 409 Conflict" $
post "/simple_pk" [json| { "k":"k1", "extra":"e1" } |] `shouldRespondWith` 409
context "attempting to insert a row with conflicting unique constraint" $
it "fails returning a 409 Conflict" $
post "/withUnique" [json| { "uni":"nodup", "extra":"e2" } |] `shouldRespondWith` 409
context "jsonb" $ do
it "serializes nested object" $ do
let inserted = [json| { "data": { "foo":"bar" } } |]
@@ -136,13 +146,6 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
, matchHeaders = ["Location" <:> [str|/json?data=eq.{"foo":"bar"}|]]
}
-- TODO! the test above seems right, why was the one below working before and not now
-- p <- request methodPost "/json" [("Prefer", "return=representation")] inserted
-- liftIO $ do
-- simpleBody p `shouldBe` inserted
-- simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%7B%22foo%22%3A%22bar%22%7D"
-- simpleStatus p `shouldBe` created201
it "serializes nested array" $ do
let inserted = [json| { "data": [1,2,3] } |]
request methodPost "/json"
@@ -153,12 +156,6 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
, matchStatus = 201
, matchHeaders = ["Location" <:> [str|/json?data=eq.[1,2,3]|]]
}
-- TODO! the test above seems right, why was the one below working before and not now
-- p <- request methodPost "/json" [("Prefer", "return=representation")] inserted
-- liftIO $ do
-- simpleBody p `shouldBe` inserted
-- simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%5B1%2C2%2C3%5D"
-- simpleStatus p `shouldBe` created201
describe "CSV insert" $ do
@@ -174,16 +171,8 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
`shouldRespondWith` ResponseMatcher {
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv"]
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
}
-- p <- request methodPost "/menagerie" [("Content-Type", "text/csv")]
-- [str|integer,double,varchar,boolean,date,money,enum
-- |13,3.14159,testing!,false,1900-01-01,$3.99,foo
-- |12,0.1,a string,true,1929-10-01,12,bar
-- |]
-- liftIO $ do
-- simpleBody p `shouldBe` "Content-Type: application/json\nLocation: /menagerie?integer=eq.13\n\n\n--postgrest_boundary\nContent-Type: application/json\nLocation: /menagerie?integer=eq.12\n\n"
-- simpleStatus p `shouldBe` created201
context "requesting full representation" $ do
it "returns full details of inserted record" $
@@ -193,21 +182,10 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
`shouldRespondWith` ResponseMatcher {
matchBody = Just "a,b\nbar,baz"
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv",
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
}
-- it "can post nulls (old way)" $ do
-- pendingWith "changed the response when in csv mode"
-- request methodPost "/no_pk"
-- [("Content-Type", "text/csv"), ("Prefer", "return=representation")]
-- "a,b\nNULL,foo"
-- `shouldRespondWith` ResponseMatcher {
-- matchBody = Just [json| { "a":null, "b":"foo" } |]
-- , matchStatus = 201
-- , matchHeaders = ["Content-Type" <:> "application/json",
-- "Location" <:> "/no_pk?a=is.null&b=eq.foo"]
-- }
it "can post nulls" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
@@ -215,7 +193,7 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
`shouldRespondWith` ResponseMatcher {
matchBody = Just "a,b\n,foo"
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv",
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
}
@@ -224,14 +202,25 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
it "fails for too few" $ do
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz"
liftIO $ simpleStatus p `shouldBe` badRequest400
-- it does not fail because the extra columns are ignored
-- it "fails for too many" $ do
-- p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz,bat,bad"
-- liftIO $ simpleStatus p `shouldBe` badRequest400
context "with unicode values" $
it "succeeds and returns usable location header" $ do
let payload = [json| { "a":"圍棋", "b":"" } |]
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
payload
liftIO $ do
simpleBody p `shouldBe` payload
simpleStatus p `shouldBe` created201
let Just location = lookup hLocation $ simpleHeaders p
r <- get location
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]"
describe "Putting record" $ do
context "to unkonwn uri" $
context "to unknown uri" $
it "gives a 404" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/fake" []
@@ -306,7 +295,7 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
describe "Patching record" $ do
context "to unkonwn uri" $
context "to unknown uri" $
it "gives a 404" $
request methodPatch "/fake" []
[json| { "real": false } |]
@@ -367,6 +356,28 @@ spec struct pool = beforeAll_ resetDb $ around (withApp cfgDefault struct pool)
[json| { id: 99 } |]
`shouldRespondWith` [json| [{id:99}] |]
it "can set a json column to escaped value" $ do
_ <- post "/json" [json| { data: {"escaped":"bar"} } |]
request methodPatch "/json?data->>escaped=eq.bar"
[("Prefer", "return=representation")]
[json| { "data": { "escaped":" \"bar" } } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{ "data": { "escaped":" \"bar" } }] |]
, matchStatus = 200
, matchHeaders = []
}
context "with unicode values" $
it "succeeds and returns values intact" $ do
void $ request methodPost "/no_pk" []
[json| { "a":"patchme", "b":"patchme" } |]
let payload = [json| { "a":"圍棋", "b":"" } |]
p <- request methodPatch "/no_pk?a=eq.patchme&b=eq.patchme"
[("Prefer", "return=representation")] payload
liftIO $ do
simpleBody p `shouldBe` "["<>payload<>"]"
simpleStatus p `shouldBe` ok200
describe "Row level permission" $
it "set user_id when inserting rows" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
+3 -8
View File
@@ -6,16 +6,11 @@ import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.Wai (Application)
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool =
beforeAll resetDb
. around (withApp (cfgLimitRows 3) struct pool) $
spec :: SpecWith Application
spec =
describe "Requesting many items with server limits enabled" $ do
it "restricts results" $
get "/items"
+61 -9
View File
@@ -6,15 +6,12 @@ import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders))
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Text.Heredoc
import Network.Wai (Application)
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool) $ do
spec :: SpecWith Application
spec = do
describe "Querying a table with a column called count" $
it "should not confuse count column with pg_catalog.count aggregate" $
@@ -249,6 +246,14 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
, matchHeaders = []
}
it "can combine multiple prefer values" $
request methodGet "/items?id=eq.5" [("Prefer","plurality=singular ; future=new; count=none")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"id":5} |]
, matchStatus = 200
, matchHeaders = []
}
it "works in the presence of a range header" $
let headers = ("Prefer","plurality=singular") :
rangeHdrs (ByteRangeFromTo 0 9) in
@@ -340,7 +345,7 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
`shouldRespondWith` ResponseMatcher {
matchBody = Just "k,extra\nxyyx,u\nxYYx,v"
, matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv"]
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
}
describe "Canonical location" $ do
@@ -373,7 +378,17 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
describe "remote procedure call" $ do
context "a proc that returns a set" $
context "a proc that returns a set" $ do
it "returns paginated results" $
request methodPost "/rpc/getitemrange"
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":3}] |]
, matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-0/2"]
}
it "returns proper json" $
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |]
@@ -383,11 +398,48 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
[json| [] |]
context "a proc that returns plain text" $
context "a proc that returns plain text" $ do
it "returns proper json" $
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
[json| [{"sayhello":"Hello, world"}] |]
it "can handle unicode" $
post "/rpc/sayhello" [json| { "name": "" } |] `shouldRespondWith`
[json| [{"sayhello":"Hello, ¥"}] |]
context "improper input" $ do
it "rejects unknown content type even if payload is good" $
request methodPost "/rpc/sayhello"
(acceptHdrs "audio/mpeg3") [json| { "name": "world" } |]
`shouldRespondWith` 415
it "rejects malformed json payload" $
request methodPost "/rpc/sayhello"
(acceptHdrs "application/json") "sdfsdf"
`shouldRespondWith` 400
context "unsupported verbs" $ do
it "DELETE fails" $
request methodDelete "/rpc/sayhello" [] ""
`shouldRespondWith` 405
it "PATCH fails" $
request methodPatch "/rpc/sayhello" [] ""
`shouldRespondWith` 405
it "OPTIONS fails" $
-- TODO: should return info about the function
request methodOptions "/rpc/sayhello" [] ""
`shouldRespondWith` 405
it "GET fails with 405 on unknown procs" $
-- TODO: should this be 404?
get "/rpc/fake" `shouldRespondWith` 405
it "GET with 405 on known procs" $
get "/rpc/sayhello" `shouldRespondWith` 405
it "executes the proc exactly once per request" $ do
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
[json| [{"callcounter":1}] |]
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
[json| [{"callcounter":2}] |]
describe "weird requests" $ do
it "can query as normal" $ do
get "/Escap3e;" `shouldRespondWith`
+99 -7
View File
@@ -6,17 +6,109 @@ import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
import Hasql as H
import Hasql.Postgres as P
import qualified Data.ByteString.Lazy as BL
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.Wai (Application)
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = beforeAll resetDb
. around (withApp cfgDefault struct pool) $
defaultRange :: BL.ByteString
defaultRange = [json| { "min": 0, "max": 15 } |]
emptyRange :: BL.ByteString
emptyRange = [json| { "min": 2, "max": 2 } |]
spec :: SpecWith Application
spec = do
describe "POST /rpc/getitemrange" $ do
context "without range headers" $ do
context "with response under server size limit" $
it "returns whole range with status 200" $
post "/rpc/getitemrange" defaultRange `shouldRespondWith` 200
context "when I don't want the count" $ do
it "returns range Content-Range with */* for empty range" $
request methodPost "/rpc/getitemrange"
[("Prefer", "count=none")] emptyRange
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns range Content-Range with range/*" $
request methodPost "/rpc/getitemrange"
[("Prefer", "count=none")] defaultRange
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10},{"id":11},{"id":12},{"id":13},{"id":14},{"id":15}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
context "with range headers" $ do
context "of acceptable range" $ do
it "succeeds with partial content" $ do
r <- request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 0 1) defaultRange
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-1/15"
simpleStatus r `shouldBe` partialContent206
it "understands open-ended ranges" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFrom 0) defaultRange
`shouldRespondWith` 200
it "returns an empty body when there are no results" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 0 1) emptyRange
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/0"]
}
it "allows one-item requests" $ do
r <- request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 0 0) defaultRange
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-0/15"
simpleStatus r `shouldBe` partialContent206
it "handles ranges beyond collection length via truncation" $ do
r <- request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 10 100) defaultRange
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "10-14/15"
simpleStatus r `shouldBe` partialContent206
context "of invalid range" $ do
it "fails with 416 for offside range" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 1 0) emptyRange
`shouldRespondWith` 416
it "refuses a range with nonzero start when there are no items" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 1 2) emptyRange
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"]
}
it "refuses a range requesting start past last item" $
request methodPost "/rpc/getitemrange"
(rangeHdrs $ ByteRangeFromTo 100 199) defaultRange
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/15"]
}
describe "GET /items" $ do
context "without range headers" $ do
context "with response under server size limit" $
it "returns whole range with status 200" $
+64 -6
View File
@@ -4,16 +4,14 @@ import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Hasql as H
import Hasql.Postgres as P
import SpecHelper
import PostgREST.Types (DbStructure(..))
import Network.HTTP.Types
import Network.Wai (Application)
spec :: SpecWith Application
spec = do
spec :: DbStructure -> H.Pool P.Postgres -> Spec
spec struct pool = around (withApp cfgDefault struct pool) $ do
describe "GET /" $ do
it "lists views in schema" $
request methodGet "/" [] ""
@@ -26,6 +24,7 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
, {"schema":"test","name":"comments","insertable":true}
, {"schema":"test","name":"complex_items","insertable":true}
, {"schema":"test","name":"compound_pk","insertable":true}
, {"schema":"test","name":"filtered_tasks","insertable":true}
, {"schema":"test","name":"ghostBusters","insertable":true}
, {"schema":"test","name":"has_count_column","insertable":false}
, {"schema":"test","name":"has_fk","insertable":true}
@@ -45,6 +44,7 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
, {"schema":"test","name":"users","insertable":true}
, {"schema":"test","name":"users_projects","insertable":true}
, {"schema":"test","name":"users_tasks","insertable":true}
, {"schema":"test","name":"withUnique","insertable":true}
] |]
{matchStatus = 200}
@@ -58,6 +58,61 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
{matchStatus = 200}
describe "Table info" $ do
it "The structure of complex views is correctly detected" $
request methodOptions "/filtered_tasks" [] "" `shouldRespondWith`
[json|
{
"pkey": [
"myId"
],
"columns": [
{
"references": null,
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "myId",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 1
},
{
"references": null,
"default": null,
"precision": null,
"updatable": true,
"schema": "test",
"name": "name",
"type": "text",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 2
},
{
"references": {
"schema": "test",
"column": "id",
"table": "projects"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "projectID",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 3
}
]
}
|]
it "is available with OPTIONS verb" $
request methodOptions "/menagerie" [] "" `shouldRespondWith`
[json|
@@ -321,3 +376,6 @@ spec struct pool = around (withApp cfgDefault struct pool) $ do
]
}
|]
it "errors for non existant tables" $
request methodOptions "/dne" [] "" `shouldRespondWith` 404
+20
View File
@@ -0,0 +1,20 @@
module Feature.UnicodeSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai (Application)
import Control.Monad (void)
spec :: SpecWith Application
spec =
describe "Reading and writing to unicode schema and table names" $
it "Can read and write values" $ do
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
`shouldRespondWith` "[]"
void $ post "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
[json| { "هویت": 1 } |]
get "/%D9%85%D9%88%D8%A7%D8%B1%D8%AF"
`shouldRespondWith` [json| [{ "هویت": 1 }] |]
+34 -15
View File
@@ -3,9 +3,14 @@ module Main where
import Test.Hspec
import SpecHelper
--import PostgREST.Types (DbStructure(..))
import qualified Hasql.Pool as P
import PostgREST.DbStructure (getDbStructure)
import PostgREST.App (postgrest)
import Data.String.Conversions (cs)
import qualified Feature.AuthSpec
import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec
import qualified Feature.InsertSpec
@@ -13,25 +18,39 @@ import qualified Feature.QueryLimitedSpec
import qualified Feature.QuerySpec
import qualified Feature.RangeSpec
import qualified Feature.StructureSpec
import qualified Feature.UnicodeSpec
main :: IO ()
main = do
setupDb
pool <- specDbPool
dbStructure <- specDbStructure pool
pool <- P.acquire (3, 10, cs testDbConn)
-- Not using hspec-discover because we want to precompute
-- the db structure and pass it to specs for speed
hspec $ specs dbStructure pool
result <- P.use pool $ getDbStructure "test"
let dbStructure = either (error.show) id result
withApp = return $ postgrest testCfg dbStructure pool
ltdApp = return $ postgrest testLtdRowsCfg dbStructure pool
unicodeApp = return $ postgrest testUnicodeCfg dbStructure pool
hspec $ do
mapM_ (beforeAll_ resetDb . before withApp) specs
-- this test runs with a different server flag
beforeAll_ resetDb . before ltdApp $
describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec
-- this test runs with a different schema
beforeAll_ resetDb . before unicodeApp $
describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec
where
specs dbStructure pool = do
describe "Feature.AuthSpec" $ Feature.AuthSpec.spec dbStructure pool
describe "Feature.CorsSpec" $ Feature.CorsSpec.spec dbStructure pool
describe "Feature.DeleteSpec" $ Feature.DeleteSpec.spec dbStructure pool
describe "Feature.InsertSpec" $ Feature.InsertSpec.spec dbStructure pool
describe "Feature.QueryLimitedSpec" $ Feature.QueryLimitedSpec.spec dbStructure pool
describe "Feature.QuerySpec" $ Feature.QuerySpec.spec dbStructure pool
describe "Feature.RangeSpec" $ Feature.RangeSpec.spec dbStructure pool
describe "Feature.StructureSpec" $ Feature.StructureSpec.spec dbStructure pool
specs = map (uncurry describe) [
("Feature.AuthSpec" , Feature.AuthSpec.spec)
, ("Feature.ConcurrentSpec" , Feature.ConcurrentSpec.spec)
, ("Feature.CorsSpec" , Feature.CorsSpec.spec)
, ("Feature.DeleteSpec" , Feature.DeleteSpec.spec)
, ("Feature.InsertSpec" , Feature.InsertSpec.spec)
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
]
+11 -65
View File
@@ -1,75 +1,33 @@
module SpecHelper where
import Network.Wai
import Test.Hspec
import Test.Hspec.Wai
import Hasql as H
import Hasql.Backend as B
import Hasql.Postgres as P
import Data.String.Conversions (cs)
import Data.Monoid
import Data.Text hiding (map)
import qualified Data.Vector as V
import Data.Time.Clock.POSIX (getPOSIXTime)
import Control.Monad (void)
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
hRange, hAuthorization, hAccept)
import Codec.Binary.Base64.String (encode)
import Data.CaseInsensitive (CI(..))
import Data.Maybe (fromMaybe)
import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS
import System.Process (readProcess)
import Web.JWT (secret)
import PostgREST.App (app)
import PostgREST.Config (AppConfig(..))
import PostgREST.Middleware
import PostgREST.Error(pgErrResponse)
import PostgREST.DbStructure
import PostgREST.Types
dbString :: String
dbString = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test"
testDbConn :: String
testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test"
cfg :: String -> Maybe Int -> AppConfig
cfg conStr = AppConfig conStr 3000 "postgrest_test_anonymous" "test" (secret "safe") 10
testCfg :: AppConfig
testCfg =
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 Nothing True
cfgDefault :: AppConfig
cfgDefault = cfg dbString Nothing
testUnicodeCfg :: AppConfig
testUnicodeCfg =
AppConfig testDbConn "postgrest_test_anonymous" "تست" 3000 (secret "safe") 10 Nothing True
cfgLimitRows :: Int -> AppConfig
cfgLimitRows = cfg dbString . Just
testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
pgSettings :: P.Settings
pgSettings = P.StringSettings $ cs dbString
specDbPool :: IO (H.Pool P.Postgres)
specDbPool = H.acquirePool pgSettings testPoolOpts
specDbStructure :: H.Pool P.Postgres -> IO DbStructure
specDbStructure pool = do
dbOrError <- H.session pool $ H.tx specTxSettings
$ getDbStructure "test"
either (fail . show) return dbOrError
withApp :: AppConfig -> DbStructure -> H.Pool P.Postgres
-> ActionWith Application -> IO ()
withApp config dbStructure pool perform = do
perform $ middle $ \req resp -> do
time <- getPOSIXTime
body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx specTxSettings
$ runWithClaims config time (app dbStructure config body) req
either (resp . pgErrResponse) resp result
where middle = defaultMiddle
testLtdRowsCfg :: AppConfig
testLtdRowsCfg =
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 (Just 3) True
setupDb :: IO ()
setupDb = do
@@ -106,15 +64,3 @@ authHeaderBasic u p =
authHeaderJWT :: String -> Header
authHeaderJWT token =
(hAuthorization, cs $ "Bearer " ++ token)
testPool :: IO(H.Pool P.Postgres)
testPool = H.acquirePool pgSettings testPoolOpts
clearTable :: Text -> IO ()
clearTable table = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ B.Stmt ("truncate table test." <> table <> " cascade") V.empty True
specTxSettings :: Maybe (TxIsolationLevel, Maybe Bool)
specTxSettings = Just (H.ReadCommitted, Just True)
+2
View File
@@ -266,6 +266,8 @@ INSERT INTO "Escap3e;" VALUES (1), (2), (3), (4), (5);
TRUNCATE TABLE "ghostBusters" CASCADE;
INSERT INTO "ghostBusters" VALUES (1), (3), (5);
TRUNCATE TABLE "withUnique" CASCADE;
INSERT INTO "withUnique" VALUES ('nodup', 'blah')
--
-- PostgreSQL database dump complete
--
+6 -1
View File
@@ -2,10 +2,11 @@
GRANT USAGE ON SCHEMA
postgrest
, test
, "تست"
TO postgrest_test_anonymous;
-- Schema test objects
SET search_path = test, pg_catalog;
SET search_path = test, "تست", pg_catalog;
GRANT ALL ON TABLE
items
@@ -28,12 +29,15 @@ GRANT ALL ON TABLE
, projects_view
, simple_pk
, tasks
, filtered_tasks
, tsearch
, users
, users_projects
, users_tasks
, "Escap3e;"
, "ghostBusters"
, "withUnique"
, "موارد"
TO postgrest_test_anonymous;
GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
@@ -41,6 +45,7 @@ GRANT INSERT ON TABLE insertonly TO postgrest_test_anonymous;
GRANT USAGE ON SEQUENCE
auto_incrementing_pk_id_seq
, items_id_seq
, callcounter_count
TO postgrest_test_anonymous;
-- Privileges for non anonymous users
+105 -10
View File
@@ -33,6 +33,13 @@ CREATE SCHEMA private;
CREATE SCHEMA test;
--
-- Name: تست; Type: SCHEMA; Schema: -; Owner: -
--
CREATE SCHEMA تست;
--
-- Name: plpgsql; Type: EXTENSION; Schema: -; Owner: -
--
@@ -50,6 +57,23 @@ CREATE TYPE jwt_claims AS (
id text
);
--
-- Name: big_jwt_claims; Type: TYPE; Schema: public; Owner: -
--
CREATE TYPE big_jwt_claims AS (
iss text,
sub text,
aud text,
exp integer,
nbf integer,
iat integer,
jti text,
role text,
"http://postgrest.com/foo" boolean
);
SET search_path = test, pg_catalog;
@@ -145,6 +169,14 @@ CREATE FUNCTION anti_id(test.items) RETURNS bigint
AS $_$ SELECT $1.id * -1 $_$;
SET search_path = تست, pg_catalog;
CREATE TABLE موارد (
هویت bigint NOT NULL
);
SET search_path = test, pg_catalog;
--
@@ -183,6 +215,43 @@ SELECT rolname::text, id::text FROM postgrest.auth WHERE id = id AND pass = pass
$$;
--
-- Name: jwt_test(); Type: FUNCTION; Schema: test; Owner: -
--
CREATE FUNCTION jwt_test() RETURNS public.big_jwt_claims
LANGUAGE sql SECURITY DEFINER
AS $$
SELECT 'joe'::text as iss, 'fun'::text as sub, 'everyone'::text as aud,
1300819380 as exp, 1300819380 as nbf, 1300819380 as iat,
'foo'::text as jti, 'postgrest_test'::text as role,
true as "http://postgrest.com/foo";
$$;
--
-- Name: reveal_big_jwt(); Type: FUNCTION; Schema: test; Owner: -
--
CREATE FUNCTION reveal_big_jwt() RETURNS TABLE (
iss text, sub text, aud text, exp bigint,
nbf bigint, iat bigint, jti text, "http://postgrest.com/foo" boolean
)
LANGUAGE sql SECURITY DEFINER
AS $$
SELECT current_setting('postgrest.claims.iss') as iss,
current_setting('postgrest.claims.sub') as sub,
current_setting('postgrest.claims.aud') as aud,
current_setting('postgrest.claims.exp')::bigint as exp,
current_setting('postgrest.claims.nbf')::bigint as nbf,
current_setting('postgrest.claims.iat')::bigint as iat,
current_setting('postgrest.claims.jti') as jti,
-- role is not included in the claims list
current_setting('postgrest.claims.http://postgrest.com/foo')::boolean
as "http://postgrest.com/foo";
$$;
--
-- Name: problem(); Type: FUNCTION; Schema: test; Owner: -
--
@@ -207,6 +276,18 @@ CREATE FUNCTION sayhello(name text) RETURNS text
$_$;
--
-- Name: callcounter(); Type: FUNCTION; Schema: test; Owner: -
--
CREATE SEQUENCE callcounter_count START 1;
CREATE FUNCTION callcounter() RETURNS bigint
LANGUAGE sql
AS $_$
SELECT nextval('test.callcounter_count');
$_$;
--
-- Name: test_empty_rowset(); Type: FUNCTION; Schema: test; Owner: -
--
@@ -540,6 +621,15 @@ CREATE TABLE simple_pk (
extra character varying NOT NULL
);
--
-- Name: users_projects; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users_projects (
user_id integer NOT NULL,
project_id integer NOT NULL
);
--
-- Name: tasks; Type: TABLE; Schema: test; Owner: -
@@ -551,6 +641,16 @@ CREATE TABLE tasks (
project_id integer
);
CREATE OR REPLACE VIEW filtered_tasks AS
SELECT id AS "myId", name, project_id AS "projectID"
FROM tasks
WHERE project_id IN (
SELECT id FROM projects WHERE id = 1
) AND
project_id IN (
SELECT project_id FROM users_projects WHERE user_id = 1
);
--
-- Name: tsearch; Type: TABLE; Schema: test; Owner: -
@@ -571,15 +671,6 @@ CREATE TABLE users (
);
--
-- Name: users_projects; Type: TABLE; Schema: test; Owner: -
--
CREATE TABLE users_projects (
user_id integer NOT NULL,
project_id integer NOT NULL
);
--
-- Name: users_tasks; Type: TABLE; Schema: test; Owner: -
@@ -599,6 +690,11 @@ CREATE TABLE "ghostBusters" (
"escapeId" integer not null references "Escap3e;"("so6meIdColumn")
);
CREATE TABLE "withUnique" (
uni text UNIQUE,
extra text
);
--
-- Name: id; Type: DEFAULT; Schema: test; Owner: -
@@ -904,7 +1000,6 @@ ALTER TABLE ONLY users_tasks
ALTER TABLE ONLY users_tasks
ADD CONSTRAINT users_tasks_user_id_fkey FOREIGN KEY (user_id) REFERENCES users(id);
--
-- PostgreSQL database dump complete
--