Compare commits

..
715 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
Joe Nelson 2c1f9e7eac v0.3.0.3 2016-01-08 18:27:14 -08: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
Joe Nelson 92df3d3243 Merge pull request #448 from chrishepner/readme-typo-fix
Fix README typo
2016-01-07 10:04:30 -08:00
Chris Hepner 90d393f968 Fix README typo 2016-01-07 09:21:49 -08:00
Joe Nelson a0b4cd6bf9 Add link to Caleb's awesome client 2016-01-01 22:42:33 -08:00
Joe Nelson 943c38125f Reorganized examples in docs menu 2015-12-30 17:30:44 -08:00
Joe Nelson 4b637bb54e Move permissions statements into user example
This makes it self-contained

Shorten blog permissions to compensate
2015-12-30 17:26:03 -08:00
Joe Nelson 576a38c407 Merge pull request #435 from davidthewatson/document-python-requests-jwt
Document python requests jwt
2015-12-30 13:36:42 -08:00
David Watson 5408ca26ad Add python client menu link 2015-12-30 16:32:53 -05:00
David Watson 0161007390 Remove extraneous newline 2015-12-30 16:26:25 -05:00
David Watson 7d343fdcca Rewrite docs for separate client repo 2015-12-30 15:47:49 -05:00
David Watson 07f63090bb Add link to python client repo 2015-12-30 14:43:22 -05:00
Joe Nelson 48f9ce114e Merge pull request #433 from calebmer/hotfix/consistent-escape
Consistent read query escaping
2015-12-29 13:07:26 -08:00
calebmer 3a3d4038cb Add tests for wierd usecases 2015-12-29 15:24:27 -05:00
calebmer 04e1186f08 Consistent read query escaping 2015-12-29 15:24:27 -05:00
Joe Nelson 651daa00d7 Merge pull request #439 from ruslantalpa/master
Fix bug in many-many relation detection
2015-12-26 08:26:54 -08:00
Ruslan Talpa 72002f452e add chengelog entry 2015-12-25 08:18:24 +02:00
David Watson 70ff55c8da Extract resultset size from Content-Range 2015-12-22 15:16:09 -05:00
David Watson 4626b4480b Update doc with pagination using Range header 2015-12-22 12:55:54 -05:00
Ruslan Talpa a61778dba0 Fix bug in many-many relation detection introduced but prev PR 2015-12-22 16:36:02 +02:00
David Watson bdfb0a7680 Layout tweaks 2015-12-22 01:26:23 -05:00
Joe Nelson 437a592c65 Merge pull request #434 from elimisteve/patch-1
examples/start.md: Fixed typo
2015-12-21 22:24:45 -08:00
David Watson ca2e140c30 Add example docs for python-requests-jwt 2015-12-22 01:21:22 -05:00
Steven Phillips 7e07ee7bea examples/start.md: Fixed typo
Should be 'results'
2015-12-21 22:17:17 -08:00
Joe Nelson 7bf65a95d8 Merge pull request #432 from diogob/external_auth_example
External auth example
2015-12-20 13:30:14 -08:00
Diogo Biazus 6534eeb1a2 Adds conclusion and note about token TTL 2015-12-20 15:04:18 -05:00
Diogo Biazus eef4e3c647 Tweaks the text, adds a design remark about SQL functions and adds the token TTL in HTTP cache headers 2015-12-20 14:45:22 -05:00
Diogo Biazus 1d848b8d72 Removes is_owner_or_admin and adds token expiration claim 2015-12-20 12:38:05 -05:00
Joe Nelson b26fbaf4db Merge pull request #427 from ruslantalpa/fix_407_week_authenticator_role
Fix errors in tests when setting authenticator to noinherit #407
2015-12-19 09:34:24 -08:00
Joe Nelson f1fbc98040 Merge pull request #429 from diogob/renames_v1schema_to_schema
Renames v1schema in app.json to make consistent with current parameter semantics
2015-12-18 14:16:03 -08:00
Diogo Biazus 48c041a99f Renames v1schema in app.json to make consistent with current parameter semantics 2015-12-18 16:51:42 -05:00
Diogo Biazus 30a844ff58 Adds External Authentication in docs' examples section 2015-12-17 00:36:58 -05:00
Joe Nelson b46b3c7b9a Conditional compilation for signal handling
Will not compile on windows
2015-12-16 18:05:48 -08:00
Joe Nelson 079cf0aa54 v0.3.0.2 2015-12-16 17:19:19 -08:00
Joe Nelson 0589ddcd90 Merge pull request #424 from calebmer/feature/select-column
Enable selection by column
2015-12-16 16:09:25 -08:00
calebmer f85975f5ad Add docs for column embedding 2015-12-16 18:37:35 -05:00
calebmer d1de6615f2 Add back in missing dependency after rebase 2015-12-16 18:37:25 -05:00
calebmer 8f0ba7c41e Unify JSON definition in tests 2015-12-16 18:24:04 -05:00
calebmer 7392d204bf More inclusive foreign key ducktyping 2015-12-16 18:24:04 -05:00
calebmer ee82ad1864 Better documentation adding relations 2015-12-16 18:24:03 -05:00
calebmer 6446fc962d Add changelog note 2015-12-16 18:24:03 -05:00
calebmer 88d4798d5e Add column select sans id support 2015-12-16 18:24:03 -05:00
calebmer abe87f2f16 Add support for selecting by column 2015-12-16 18:24:02 -05:00
Joe Nelson b08a402df8 Merge pull request #417 from ruslantalpa/fix_399_remove_returning
Fix #399 insert records in tables with no SELECT privileges
2015-12-16 11:35:50 -08:00
Joe Nelson f95b501232 Update stack installation instructions 2015-12-16 09:34:28 -08:00
Ruslan Talpa 66a34fccdf Fix errors in tests when setting authenticator to noinherit 2015-12-16 11:44:28 +02:00
Ruslan Talpa 4544ce3255 Fix #399 insert records in tables with no SELECT privileges 2015-12-16 10:16:14 +02:00
Joe Nelson 4bc4a68051 Merge pull request #425 from ruslantalpa/fix_414_revert_count_query_no_cte
#414 revert to separate count query
2015-12-15 12:06:19 -08:00
Ruslan Talpa 1acd07cb61 fix #414 revert to separate count query 2015-12-15 12:04:04 +02:00
Joe Nelson 2cf903cebe Merged feature/default-schema 2015-12-14 08:49:52 -08:00
Joe Nelson 9ee2b74a5d Merge pull request #422 from calebmer/feature/always-quote
`pgFmtIdent` always quotes #388
2015-12-13 19:19:38 -08:00
calebmer 6ef9000b31 pgFmtIdent always quotes #388 2015-12-13 16:58:56 -05:00
calebmer bacc899fb4 Change default schema #389 2015-12-13 16:36:32 -05:00
Joe Nelson 7f0dc82d7a Beginning of security docs 2015-12-11 22:28:00 -08:00
Joe Nelson 155e2d1c0c Merge pull request #406 from begriffs/redocument-brew
Add brew install instructions back
2015-12-11 09:45:37 -08:00
Joe Nelson ccadcec6d5 Merge branch 'spec-cache-dbstructure' 2015-12-11 09:42:26 -08:00
Joe Nelson 02f707cd5c Merge pull request #421 from ruslantalpa/spec-cache-dbstructure
Fix failing test after test structure optimization
2015-12-11 09:37:51 -08:00
Joe Nelson 848c7da4d9 Merge pull request #420 from NikolayS/patch-1
ordering with DESC & NULLS LAST together
2015-12-11 09:24:54 -08:00
Ruslan Talpa 3cd01f9859 Fix failing test after test structure optimization 2015-12-11 10:23:11 +02:00
NikolayS aed97d97f8 ordering with DESC & NULLS LAST together
One more sample query which is probably one of most useful ones in real life (by default, DESC ordering leads to NULLS FIRST option, this is not so clear for beginners), showing how to combine ordering direction and NULLS *** for the same column.
2015-12-11 09:22:45 +03:00
Joe Nelson b3144aee15 Cache schema structure between tests for speed
Causes one test to fail
2015-12-10 13:36:28 -08:00
Joe Nelson 87ceac7b00 Document basic_auth.current_email() 2015-12-10 11:23:10 -08:00
Ruslan Talpa a6512a2a69 Test and fixes for #407 #403 #107 2015-12-09 20:59:42 +02:00
Joe Nelson 6f7bf30a2d Merge pull request #416 from ruslantalpa/fix_396_nullable_fk
Fix #396 include records with missing parents and return [] instead of  null for missing children
2015-12-09 10:29:45 -08:00
Ruslan Talpa dad41cd3ed Fix #396 include records with missing parents and remove CTE related to parent relation 2015-12-09 14:21:27 +02:00
Joe Nelson 52849065cc Merge pull request #410 from diogob/refactor_tests
Refactor tests
2015-12-08 13:43:02 -08:00
Joe Nelson 728decb0b8 Change brew install time estimate 2015-12-08 10:31:36 -08:00
Diogo Biazus 4cc0189ac9 Prefixes all roles used in test suite with postgrest_test 2015-12-08 12:03:46 -05:00
Diogo Biazus 8b4cb4be6e Replaces postgrest_authenticator for postgrest_test_authenticator 2015-12-08 11:53:18 -05:00
Diogo Biazus d95aeb15f9 Reduces privileges fixture to a smaller file so it's manageable without the use of pg_dump 2015-12-08 11:50:13 -05:00
Diogo Biazus 6cc8ca707f Separates privileges in another fixture 2015-12-08 11:33:22 -05:00
Diogo Biazus 63805d05a1 Removes GRANT ALL and let Structure spec pending 2015-12-08 11:11:48 -05:00
Diogo Biazus f66f9e7c92 Adds -d postgrest to setup database step in SpecHelper 2015-12-08 11:06:50 -05:00
Joe Nelson 213d86f3e3 Merge pull request #409 from begriffs/ctrl-c
Release db connections on sigint
2015-12-07 23:46:37 -08:00
Joe Nelson 969a95b29b Remove shutdown message which spoils log format 2015-12-07 23:34:34 -08:00
Diogo Biazus ca3ab7babf Cleanup code in SpecHelper 2015-12-08 01:01:38 -05:00
Diogo Biazus 0961524e70 Refactors tests to use new fixtures and remove some haskell functions that wont be necessary anymore 2015-12-08 00:44:49 -05:00
Diogo Biazus 027bbc6074 Adds separate fixture file with commands to create test database 2015-12-08 00:00:00 -05:00
Diogo Biazus c1c44aae05 Adds separate fixture file with data used in specs 2015-12-07 23:59:33 -05:00
Joe Nelson 3bc1ad0133 Changelog 2015-12-07 16:55:45 -08:00
Joe Nelson ca5078b4f8 Alphabetize other imports 2015-12-07 16:54:59 -08:00
Joe Nelson 67a3194903 Release all db connections on ctrl-c
Fixes #268
2015-12-07 16:54:59 -08:00
Joe Nelson 24b7a7d3e9 Merge pull request #403 from ruslantalpa/master
Detect relations event when authenticator does not have rights to intermediate tables
2015-12-07 15:06:21 -08:00
Joe Nelson 0e56bff5d6 Revert "Removed brew install instructions"
I know how to release a new brew formula so it won't fall out of date
2015-12-07 13:46:06 -08:00
Joe Nelson b8b5fac03c Merge pull request #404 from begriffs/max-rows
Add --max-rows option
2015-12-06 15:57:37 -08:00
Joe Nelson e88fa7db31 Docs and changelog 2015-12-05 19:59:30 -08:00
Joe Nelson 598b2bfbda Remove lint 2015-12-05 19:50:07 -08:00
Joe Nelson f4c73c7666 Test for --max-rows behavior 2015-12-05 19:31:53 -08:00
Joe Nelson cbbb1871bb Add --max-rows option (no automated tests yet)
Fixes #288
2015-12-05 19:03:18 -08:00
Ruslan Talpa 093874469e Detect relations event when authenticator does not have rights to intermediate tables 2015-12-04 17:45:50 +02:00
Joe Nelson d21120962d Blog example 2015-12-03 21:58:55 -08:00
Joe Nelson 27ad4641d0 Merge pull request #400 from diogob/order_by_computed_column
Order by computed column
2015-12-02 19:19:02 -08:00
Diogo Biazus 65e7744858 Uses anti_id computed column to make test case clearer 2015-12-02 17:12:37 -05:00
Diogo Biazus c2e3dd716c Updates CHANGELOG 2015-12-02 15:28:27 -05:00
Diogo Biazus 1266bad2f8 Adds another note about computed coulmns to the docs 2015-12-02 15:27:27 -05:00
Diogo Biazus 71dab115c9 Uses qualified column name in order by clause to allow computed columns 2015-12-02 15:23:10 -05:00
Joe Nelson 205aec20fe Note min postgres version in docs 2015-11-30 21:10:15 -08:00
Joe Nelson 83fa3070fb Add intro video to docs 2015-11-30 14:30:51 -08:00
Joe Nelson a2870494c9 Merge pull request #397 from begriffs/bye-bcrypt
bcrypt is no longer a required dependency
2015-11-29 23:38:37 -08:00
Joe Nelson 614b4dfbab Remove deps deemed unnecessary by packunused
Also sort build-depends
2015-11-29 23:32:13 -08:00
Joe Nelson cbc6685725 bcrypt is no longer a required dependency
Fixes #395
2015-11-29 23:02:34 -08:00
Joe Nelson f17d47790a Better docs for command line usage
Mentions #277 and how to prevent it
2015-11-29 22:46:34 -08:00
Joe Nelson 334f900e15 Ask for sql logs in bug reports 2015-11-29 19:44:57 -08:00
Joe Nelson 6804d91f8d Merge pull request #393 from begriffs/user-mgmt-docs
Document how to build username/pw auth on top of jwt
2015-11-29 12:48:15 -08:00
Joe Nelson 54bf0b460f Finish auth example docs 2015-11-29 12:43:44 -08:00
Joe Nelson fa281fe59c WIP: documenting how to build username/pw auth on top of jwt 2015-11-28 16:07:51 -08:00
Joe Nelson 048a8531f2 Merge pull request #392 from calebmer/hotfix/jwt-time
Fix time fetch on initialization
2015-11-28 15:06:41 -08:00
Caleb Meredith ed51502387 Add unreleased line to top of changelog 2015-11-28 18:03:27 -05:00
Caleb Meredith e7a47215f0 Update CHANGELOG.md 2015-11-28 17:56:37 -05:00
calebmer b04e2ec663 Change updated package error to warning 2015-11-28 17:41:54 -05:00
calebmer b651a45734 Fix time fetch on initialization 2015-11-28 16:49:12 -05:00
Joe Nelson 1a54135f0d Add authenticator role to blog schema 2015-11-27 23:23:23 -08:00
Joe Nelson c6f69956f6 Inline fk constraints in example 2015-11-27 23:07:29 -08:00
Joe Nelson c681ff2d9d Update reading/writing docs 2015-11-27 22:59:11 -08:00
Joe Nelson 5e22538684 Link to Ruslan's blog demo 2015-11-27 22:59:11 -08:00
Joe Nelson d6d0ba524f Removed brew install instructions
The brew formula gets outdated
2015-11-27 22:59:11 -08:00
Joe Nelson cd53402ace Update the getting started example for v0.3 2015-11-27 22:59:07 -08:00
Joe Nelson 19b856db3b v0.3.0.1 2015-11-27 13:04:47 -08:00
Joe Nelson 4b911263e3 Merge pull request #386 from ruslantalpa/master
bugfix + Disambiguate range selection when plurality=singular
2015-11-27 00:49:59 -08:00
Ruslan Talpa c0fa5c3d4a changelog entry 2015-11-25 10:13:34 +02:00
Ruslan Talpa b3055888c5 Merge remote-tracking branch 'begriffs/master' 2015-11-25 10:10:11 +02:00
Ruslan Talpa 48ee77a64d bugfix: filter columns on embeded parent objects 2015-11-24 12:53:50 +02:00
Joe Nelson 6f9fc7adac Mark release in changelog 2015-11-24 00:08:31 -08:00
Ruslan Talpa f678dff735 disambiguate range selection when plurality=singular 2015-11-24 09:39:44 +02:00
Joe Nelson e623cf8198 Coming in for landing on v0.3 2015-11-23 23:09:59 -08:00
Joe Nelson 0fd02ddc1d Merge pull request #385 from begriffs/03readme
Update readme for v3
2015-11-23 23:03:53 -08:00
Joe Nelson 8250008130 Update readme for v3
Link to the new docs, not wikis
2015-11-23 22:50:43 -08:00
Joe Nelson 377aeefde9 Test tricky situation for plurality=singular 2015-11-23 17:15:09 -08:00
Joe Nelson 1170137e7f Merge pull request #377 from begriffs/simplify
Disentangle code
2015-11-23 12:49:54 -08:00
Joe Nelson a5e7d9aff0 Update changelog 2015-11-23 11:15:30 -08:00
Joe Nelson ae80d08484 Merge pull request #384 from diogob/reduces_querybuilder_interface
Reduces querybuilder interface
2015-11-23 11:04:50 -08:00
Joe Nelson 029276bd62 Use brackets to specify subqueries 2015-11-23 10:58:04 -08:00
Diogo Biazus addd47c09f Adds haddock string to QueryBuilder module 2015-11-23 13:56:47 -05:00
Diogo Biazus be9cce0043 Moves createReadStatement and createWriteStatement to QueryBuilder since they build queries. Also reduces QueryBuilder module interface. 2015-11-23 13:45:10 -05:00
Joe Nelson 64da08b220 Merge pull request #383 from ruslantalpa/simplify
Type refactoring (ApiRequest/DbRequest)
2015-11-23 09:31:01 -08:00
Ruslan Talpa 50745ad48b Type refactoring (ApiRequest/DbRequest) 2015-11-23 10:12:29 +02:00
Joe Nelson 7f58600dbb All green
But still unsightly
2015-11-22 22:38:54 -08:00
Joe Nelson bc552848ba Prevent inserting CSV with varying row length 2015-11-22 21:51:42 -08:00
Joe Nelson 936a368be7 Update only the columns specified in the json payload 2015-11-22 17:46:00 -08:00
Joe Nelson d03d68c25f Simplify checking object keys for equality 2015-11-22 15:00:28 -08:00
Joe Nelson 1f6fc5cbd8 Ensure JSON payload objects all have same keys 2015-11-22 13:51:46 -08:00
Joe Nelson 60007b5f10 WIP: Insertion memory leak fixed
But a whole lot of other things broken, including updates
2015-11-20 13:08:23 -08:00
Joe Nelson f54742186b Use show instances for a shortcut 2015-11-20 10:29:08 -08:00
Joe Nelson d2d86b35c4 Merge pull request #380 from ruslantalpa/simplify
Get the refactored code to compile and pass tests
2015-11-20 09:07:29 -08:00
Ruslan Talpa 78fd766de3 small refactor 2015-11-20 18:19:13 +02:00
Ruslan Talpa cf2e45d47f Fix lint issue 2015-11-20 16:11:21 +02:00
Ruslan Talpa 0cce22f8c1 check request payload for structure & remove bad test for csv 2015-11-20 16:07:20 +02:00
Ruslan Talpa aa2f0287b1 Fix RPC failing tests 2015-11-20 15:50:53 +02:00
Ruslan Talpa f18cfbd7f4 Stricter pattern matching & case branches rearangement + remove a few small functions 2015-11-20 15:36:04 +02:00
Ruslan Talpa f5bb898992 delete commented code 2015-11-20 14:10:28 +02:00
Ruslan Talpa 7f52430e0e Get the refactored code to compile (5 test failing) 2015-11-20 14:04:00 +02:00
Joe Nelson c80ab6be0f WIP: converting App 2015-11-19 20:45:24 -08:00
Joe Nelson b6a58935f3 Greater type safety for OrderTerm 2015-11-19 11:21:27 -08:00
Joe Nelson f3d5759c1a JWT is handled by middleware, do not need it in Intent 2015-11-19 11:20:30 -08:00
Joe Nelson 87c946ef52 Treat JWT as a Secret, not String 2015-11-19 09:58:41 -08:00
Joe Nelson 2c3a52fc35 Cool person style for long comments 2015-11-19 00:03:48 -08:00
Joe Nelson 496cd77510 More docs for pickContentType 2015-11-18 23:56:59 -08:00
Joe Nelson 8f04967103 Provide more context about unknown routes 2015-11-18 23:51:59 -08:00
Joe Nelson b260cff6fd Provide more context for unknown actions 2015-11-18 23:51:38 -08:00
Joe Nelson ffeb02e72e Include request payload in userIntent 2015-11-18 23:43:47 -08:00
Joe Nelson ed18f2b1e8 Function to convert parsed CSV to array of JSON objects 2015-11-18 20:21:21 -08:00
Joe Nelson a0af096ff4 WIP: translate HTTP request into user intent
The app code will base its logic off the intent data
2015-11-18 19:18:13 -08:00
Joe Nelson c21e5a9fc8 Merge pull request #375 from calebmer/feature/plurality-singular
Implement single route selection
2015-11-18 14:39:33 -08:00
calebmer 79caba278e Add changelog entry 2015-11-18 17:30:37 -05:00
Joe Nelson 80da27237d Merge pull request #376 from ruslantalpa/master
Refactor parseRequest buildSelectApiRequest, add buildMutateApiRequest function
2015-11-17 12:44:29 -08:00
Ruslan Talpa f24dd90e51 refactor buildSelectApiRequest 2015-11-17 14:19:23 +02:00
Ruslan Talpa 17920b85c7 Refactor parseRequest, add buildMutateApiRequest function 2015-11-17 10:26:06 +02:00
Joe Nelson ffd45ef0b5 Merge pull request #374 from calebmer/feature/error-refactor
Small error refactor
2015-11-16 14:50:54 -08:00
calebmer ad555986d4 Error refactors 2015-11-16 17:28:39 -05:00
calebmer 54eb0d3ec4 Implement single route selection 2015-11-16 17:21:07 -05:00
Joe Nelson aa87853e71 Merge pull request #373 from begriffs/schema-templates
Schema templates
2015-11-16 13:27:32 -08:00
Joe Nelson 65c5054c93 Merge pull request #372 from ruslantalpa/app_refactor
App.hs refactor (3)
2015-11-16 13:21:22 -08:00
Joe Nelson 8711362cf6 Merge pull request #370 from ruslantalpa/add_type_syn
Add type synonyms to to beter explain things that used to be Text (2)
2015-11-16 08:52:06 -08:00
Joe Nelson 57aad6baa6 Merge pull request #369 from ruslantalpa/master
Clean up things like T.Text T.intercalate in QueryBuilder (1)
2015-11-16 08:49:22 -08:00
Ruslan Talpa 0c3545fa09 App.hs refactor 2015-11-16 12:26:55 +02:00
Ruslan Talpa 75646247f2 Add type synonims to to beter explain things that used to be Text 2015-11-16 10:40:46 +02:00
Ruslan Talpa 3152b24d3f Clean up things like T.Text T.intercalate in QueryBuilder 2015-11-16 09:55:10 +02:00
Joe Nelson 8171a9959d Merge pull request #363 from diogob/refactor_query_builder
Merge PgQuery and QueryBuilder...
2015-11-15 10:30:29 -08:00
Joe Nelson c4018ad882 Merge pull request #366 from calebmer/patch-1
Summarize column synonym change
2015-11-15 09:21:52 -08:00
Caleb Meredith 62951f29ac Update CHANGELOG.md 2015-11-15 11:05:52 -05:00
Diogo Biazus 9dc3399d4a Adds Schema type alias back to QueryBuilder 2015-11-15 10:18:34 -05:00
Joe Nelson e0c99f52e0 Merge pull request #364 from ruslantalpa/master
Shape mutation response feture mention in changelog
2015-11-14 23:04:29 -08:00
Ruslan Talpa 6c0966f656 shape mutation response feture mention in changelog 2015-11-15 08:27:34 +02:00
Diogo Biazus 9da7db9e09 Moves some top level bindings to 'where' when it makes sense 2015-11-14 23:08:35 -05:00
Diogo Biazus f47d5e52f4 Moves all PgQuery module to QueryBuilder 2015-11-14 22:54:55 -05:00
Diogo Biazus fbe6600ff1 Makes exports explicit and move some functions around 2015-11-14 22:43:03 -05:00
Joe Nelson a6cfecbc29 Merge v3 2015-11-14 17:52:05 -08:00
Joe Nelson 51e1d8d796 Merge pull request #353 from calebmer/feature/view-relations
Better view relations
2015-11-14 10:00:46 -08:00
calebmer 0bf3dd9b1c Remove extraneous files 2015-11-14 08:02:09 -05:00
Joe Nelson 98caf9e091 Light validation on email column 2015-11-13 13:16:40 -08:00
Joe Nelson 71e6d0414d Inline constraints 2015-11-13 12:59:45 -08:00
calebmer 3794d358b4 Prettify monads 2015-11-12 15:09:28 -05:00
calebmer 2f6254f44c Rename DbStructure names 2015-11-12 15:01:23 -05:00
Joe Nelson 6cc493da6b Version 0.2.12.1 2015-11-12 11:06:11 -08:00
Joe Nelson e8bac0c739 Merge pull request #355 from ruslantalpa/v3
Small bug fix introduced by the last JWT PR
2015-11-12 08:40:21 -08:00
calebmer 0f5bc34c04 Remove fromJust assumptions 2015-11-12 08:22:27 -05:00
Ruslan Talpa 6e9a28ba3c fix the missing status400 include 2015-11-12 15:19:11 +02:00
calebmer acb8f8a154 Fix failing insert test 2015-11-12 08:14:14 -05:00
Ruslan Talpa eb94c507f9 remove secure flag from debian config 2015-11-12 15:07:40 +02:00
calebmer 1061854f35 Resolve SQL errors 2015-11-12 07:38:11 -05:00
calebmer b8b073810f Rename functions 2015-11-11 15:53:52 -05:00
Joe Nelson 22c50392e5 Merge pull request #344 from calebmer/feature/jwt-expire
Ensure JWT expires
2015-11-11 08:31:20 -08:00
Joe Nelson 80928535b0 Active flag is more accurately called verified 2015-11-11 08:15:06 -08:00
calebmer 0374d4e651 Add tables back to DbStructure 2015-11-11 10:32:19 -05:00
calebmer 16af8fc61e Better relations for views
- Better column synonyms detection
- Raise relations to accessible schema when possible
2015-11-11 09:11:57 -05:00
calebmer e1e4fe6d5c Types reference each other 2015-11-11 09:09:53 -05:00
calebmer 3a682360f3 Remove view relations from sql statement 2015-11-11 09:03:06 -05:00
calebmer 7560fafbab Refactor DbStructure
- Rename `dbstructure` to `db` in App.hs
- Rename PgStructure* to DbStructure*
- Move `DbStructure` creation to DbStructure.hs
2015-11-11 09:03:06 -05:00
calebmer 62cb8e0453 Cleanup JWT expires 2015-11-11 08:34:50 -05:00
Joe Nelson 3f1d2d8ed9 Merge pull request #342 from ruslantalpa/v3
Refactoring & 	Revert to the old way of displaying the list of tables
2015-11-11 01:03:07 -08:00
Joe Nelson ad316841f2 Restrict possible values of token_type 2015-11-10 21:09:56 -08:00
Joe Nelson 807e4b7787 Turn signup() into a plain sql function 2015-11-10 21:07:14 -08:00
Joe Nelson 91f15aa0e3 Use uuid type for token and name for role 2015-11-10 21:00:50 -08:00
Joe Nelson bda5f0a176 Match trigger names with their functions 2015-11-10 21:00:50 -08:00
Joe Nelson 86d992c21f Roles 2015-11-10 21:00:50 -08:00
Joe Nelson a508f8df37 Remove usernames, just use email 2015-11-10 21:00:50 -08:00
Joe Nelson 1b28f77854 Allow authors to see only themselves in /users 2015-11-10 21:00:50 -08:00
Joe Nelson c8479e792f Actual blog and row level security! 2015-11-10 21:00:50 -08:00
Joe Nelson 22fb13b30a Shorten login function to login 2015-11-10 21:00:50 -08:00
Joe Nelson 9daaf6ba70 s/logins/users 2015-11-10 21:00:50 -08:00
Joe Nelson 64172873e4 Explanation about split schemas 2015-11-10 21:00:50 -08:00
Joe Nelson 3a659843d2 Limit field lengths in logins
It is open to the public so people could abuse the storage

Also switch to text type everywhere
2015-11-10 21:00:49 -08:00
Joe Nelson 9fa16e2053 Handle missing "active" key in logins update
The default value of underlying column gets clobbered by triggers
2015-11-10 21:00:49 -08:00
Joe Nelson c25cd1b97e Use uuid-ossp extension rather than custom function 2015-11-10 21:00:49 -08:00
Joe Nelson 1e017b86d3 Drop trigger before (re)creating it 2015-11-10 21:00:49 -08:00
Joe Nelson 1524a4fe74 User management view and trigger 2015-11-10 21:00:49 -08:00
Joe Nelson d4ef343b8d Leave public functions in current schema
Put supporting things into basic_auth schema

Also tested from a clean db
2015-11-10 21:00:49 -08:00
Joe Nelson 2687ad63dd Basic tables/procs for basic auth 2015-11-10 21:00:49 -08:00
Joe Nelson 08fc83709c Merge pull request #352 from ruslantalpa/master
A simpler way for non haskell devs to install postgrest
2015-11-10 08:28:33 -08:00
Ruslan Talpa 92174dc0d2 modify build from source steps to also copy the bin to the right location (remove small comment) 2015-11-10 10:31:22 +02:00
Ruslan Talpa 2d72847d39 modify build from source steps to also copy the bin to the right location 2015-11-10 10:30:07 +02:00
Ruslan Talpa 1b402abaf2 Merge remote-tracking branch 'begriffs/v3' into v3 2015-11-10 10:22:06 +02:00
Joe Nelson 368bf34842 Merge pull request #345 from calebmer/feature/insecure-forbidden
Do not redirect insecure requests
2015-11-08 10:24:01 -08:00
calebmer c024687629 Remove secure flag entirely 2015-11-08 13:03:50 -05:00
Joe Nelson 4f7dd12133 Merge pull request #348 from xDAGRONx/homebrew
Add documentation for installing via Homebrew
2015-11-06 10:12:19 -08:00
JC Wilcox 8b16a1ee33 Add documentation for installing via Homebrew
Modify the server installation guide to include steps for installation
using Hombrew on OS X.
2015-11-06 10:23:33 -06:00
Joe Nelson e210283a70 Merge pull request #346 from begriffs/jwt-proc-return
Encode JWT when proc return types end in jwt_claims
2015-11-05 17:58:45 -08:00
Joe Nelson 3d2a78e962 Encode JWT when proc return types end in jwt_claims
Fixes it when the jwt_claims type is defined in a non-default schema
2015-11-05 17:46:33 -08:00
calebmer 34c153086c Do not redirect insecure requests 2015-11-05 17:42:48 -05:00
calebmer aab2f0d1f1 Ensure JWT expires 2015-11-05 17:04:54 -05:00
Ruslan Talpa cfad68f5cb Fix test 2015-11-04 16:28:22 +02:00
Ruslan Talpa 37b1d7d692 fix debian scripts to match current parameters 2015-11-04 16:23:02 +02:00
Ruslan Talpa 28b7b80bbb remove PUT from cors 2015-11-04 16:22:13 +02:00
Ruslan Talpa 14e806759d Revert to the old way of displaying the list of tables 2015-11-03 09:31:56 +02:00
Joe Nelson 5e9d29e07b Merge pull request #340 from ruslantalpa/master
Fix for #334
2015-11-02 17:27:33 -08:00
Ruslan Talpa 5113187bb1 changelog update and lint fix 2015-11-02 23:35:59 +02:00
Ruslan Talpa b3e88a37d3 Refacttoring 2015-11-02 23:31:35 +02:00
Joe Nelson 4803d7c828 Merge pull request #339 from ruslantalpa/v3
Refactor to add operators in just one place, @> and <@ operators for …
2015-11-02 09:44:06 -08:00
Ruslan Talpa ef08af359c Fix for #334 2015-11-02 14:44:45 +02:00
Ruslan Talpa 2e4c862d25 Refactor to add operators in just one place, @> and <@ operators for #338 and #181 2015-11-02 10:49:22 +02:00
Joe Nelson 6b118819d7 Merge pull request #337 from diogob/changes_stack_resolver
Changes stack resolver to nightly.
2015-11-01 15:15:11 -08:00
Diogo Biazus 2c1236652e Fixes hlint suggestions 2015-11-01 16:49:21 -05:00
Diogo Biazus 066d120c0e Changes stack resolver to nightly. 2015-10-31 12:01:26 -04:00
Joe Nelson 6b7e833023 Merge pull request #331 from ruslantalpa/v3
Code refactoring POST/PUT/PATCH to be more like GET and use internal data type
2015-10-31 00:02:39 -07:00
Joe Nelson 31c3edc183 Merge pull request #335 from PierreR/jsonb
Add a test case for issue #334
2015-10-30 13:10:51 -07:00
Pierre Radermecker 2cfe3651b4 Add a test case for issue #334 2015-10-30 19:16:59 +01:00
Ruslan Talpa 246c47dba4 cleanup 2015-10-30 12:19:35 +02:00
Ruslan Talpa 6fd0d5648f PUT path commented, DELETE rewritten, all statementT functions commented 2015-10-30 12:08:25 +02:00
Ruslan Talpa 738989c375 Cleanup 2 2015-10-29 16:22:31 +02:00
Ruslan Talpa 9458ee3292 Cleanup / Refactoring 2015-10-29 16:04:16 +02:00
Joe Nelson 1c22b8d429 Merge pull request #333 from diogob/fix_empty_set_returning_function
Fixes 500 when set returning function returns an empty row set [fix #332]
2015-10-28 10:57:05 -07:00
Diogo Biazus c6956d0ff6 Adds fix to CHANGELOG 2015-10-28 09:17:25 -04:00
Ruslan Talpa 915ce0fa9d Fix for detecting many2many relations when the link table for more then 2 tables 2015-10-28 10:41:16 +02:00
Diogo Biazus 4369a5617e Fixes 500 when set returning function returns an empty row set [fix #332] 2015-10-27 21:19:55 -04:00
Ruslan Talpa 482a43d722 PATCH path rewriten in new style 2015-10-27 16:27:44 +02:00
Ruslan Talpa f7e6005087 cleanup 2015-10-27 14:14:04 +02:00
Ruslan Talpa f02b8381ea shape the response after inserting 2015-10-27 13:16:14 +02:00
Ruslan Talpa 58d009b388 Merge branch 'v3' of https://github.com/ruslantalpa/postgrest into v3 2015-10-26 16:21:09 +02:00
Ruslan Talpa d43bac6e8f Code cleanup (lint suggestions) 2015-10-26 16:19:54 +02:00
Ruslan Talpa d8b7332acc Code cleanup (lint suggestions) 2015-10-26 15:27:28 +02:00
Ruslan Talpa 1fdb700bc8 Fix a few tests 2015-10-26 13:50:56 +02:00
Joe Nelson bd1826670e Version 0.2.12.0 2015-10-25 11:46:16 -07:00
Ruslan Talpa c265cf829b Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-24 23:07:08 +03:00
Ruslan Talpa 5390fb702d Fix for #321 2015-10-24 23:06:19 +03:00
Joe Nelson aab1500879 Merge pull request #330 from ruslantalpa/master
Fix for #321
2015-10-24 12:57:31 -07:00
Ruslan Talpa 0ed8ec9868 Fix for #321 2015-10-24 22:50:03 +03:00
Ruslan Talpa 00c8303fed Merge branch 'v3' of https://github.com/ruslantalpa/postgrest into v3 2015-10-24 21:26:19 +03:00
Ruslan Talpa cd3a149aa4 Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-24 21:25:01 +03:00
Joe Nelson 97d612a60d Merge pull request #327 from diogob/update_stack
Update stack resolver
2015-10-23 13:43:29 -07:00
Ruslan Talpa c6d40fff8c Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-23 23:39:37 +03:00
Diogo Biazus 5de9db0ca1 Updates stack resolver to 3.10 and adds new hspec to custom build plan 2015-10-23 15:44:18 -04:00
Joe Nelson a624eb08ce Merge pull request #326 from begriffs/710-only
GHC 7.10 only
2015-10-23 11:22:45 -07:00
Joe Nelson af15046d3f Use newer hspec 2015-10-23 11:08:59 -07:00
Joe Nelson 7692693aae Enforce GHC >= 7.10 and fix Stack warnings 2015-10-23 10:33:27 -07:00
Ruslan Talpa e40dcb1324 simplify operator formatting function 2015-10-23 12:53:31 +03:00
Ruslan Talpa 826de74a5d rearange paths to put the most used ones at the top in the case expression 2015-10-23 12:31:46 +03:00
Ruslan Talpa f5fb78ec99 version changed to 3, circle ci to use ghc 7.10.1, stricter import/export in PgQuery and remove of dead code 2015-10-23 12:23:46 +03:00
Ruslan Talpa c606149c43 code cleanup 2 2015-10-23 10:45:35 +03:00
Ruslan Talpa d4a8716a0e code cleanup 2015-10-23 10:13:51 +03:00
Ruslan Talpa 21bd921ee7 Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-23 09:15:00 +03:00
Joe Nelson 8b28b33da3 Merge pull request #320 from diogob/jwt_generic_variables
Jwt generic variables
2015-10-22 21:53:53 -07:00
Diogo Biazus 91dfd47f1d Adds another test case for jwt with empty claims 2015-10-22 23:20:16 -04:00
Diogo Biazus aad19b53c7 Includes one test case for recovering from from 400 error and another for invalid JWT tokens 2015-10-22 23:12:14 -04:00
Ruslan Talpa cea4cc5860 a bit of warning cleanup 2015-10-22 18:01:47 +03:00
Ruslan Talpa ca4014f751 a few more tests fixed (2) 2015-10-22 17:03:06 +03:00
Ruslan Talpa 2662e24991 a few more tests fixed 2015-10-22 16:17:23 +03:00
Ruslan Talpa 2b8f5f791a using query fragments instead of query transformers o generate queries 2015-10-22 13:47:34 +03:00
Joe Nelson df00d728ff Docs about methods for updating records 2015-10-21 21:12:27 -07:00
Diogo Biazus d000a6c61a Eliminates SET role duplication and changes Auth module interface 2015-10-22 00:08:02 -04:00
Ruslan Talpa 71ef03070e POST path modified with internal data type but tests failing (no Location and data returned as array) 2015-10-21 12:46:01 +03:00
Joe Nelson 9fad028074 Document API read requests 2015-10-20 19:25:31 -07:00
Diogo Biazus 81ee7cbd5e Removes redundant do 2015-10-20 19:54:29 -04:00
Diogo Biazus 250a4dcfb2 Adds back import to make GHC 7.8 happy 2015-10-20 19:51:25 -04:00
Diogo Biazus 6e55017f96 Cleans and adds haddock comments 2015-10-20 19:48:54 -04:00
Diogo Biazus a192cadced All green :D 2015-10-20 19:23:12 -04:00
Diogo Biazus 044e3865ac Removes 'Prefer: return=jwt' header and chooses jwt return based on function type 2015-10-20 01:03:11 -04:00
Diogo Biazus 2ea7bc29c6 Moves function from top level to where 2015-10-20 00:43:21 -04:00
Joe Nelson e0fe610d7b Merge pull request #324 from begriffs/mkdocs
Documentation outline
2015-10-19 16:51:53 -07:00
Joe Nelson 20198367da Documentation outline 2015-10-19 16:45:52 -07:00
Diogo Biazus a3aba84ba8 Fixes linter suggestions 2015-10-18 20:10:26 -04:00
Diogo Biazus 4e9afc8096 Adds import needed by ghc 7.8 2015-10-18 20:05:34 -04:00
Diogo Biazus f56efeb039 Reduces conde duplication assimbling response vody for RPC 2015-10-18 20:02:33 -04:00
Diogo Biazus 8b5f4e8556 Removes lenses and uses simpler approach to generate JWT claims. Also fixes the setVar to avoid the ::unknown type cast from insertableValue 2015-10-18 17:14:11 -04:00
Diogo Biazus 7ef5b7b43a Simplifies pattern matching using insertableValue and quote variable as identifier. 2015-10-18 14:03:43 -04:00
Diogo Biazus 241a38e958 Makes JWT generation possible in RPC endpoints
Fixes SET execution to execute in separate statements as Hasql uses
prepared statements we need to send 1 commend per statement.
2015-10-18 02:30:32 -04:00
Diogo Biazus aae55e0282 Make complete match agains ClaimsMap in setVar 2015-10-17 20:41:42 -04:00
Diogo Biazus 31f1a30d6f Cleans imports in Middleware and define exports in Auth 2015-10-16 15:56:07 -04:00
Diogo Biazus 275002e25d Adds dbrole filter back to root path querying the database 2015-10-16 15:55:56 -04:00
Diogo Biazus 3add3f5b6c First draft of big auth simplification 2015-10-16 15:55:47 -04:00
Ruslan Talpa 1f80b806bd data types refactoring 2015-10-16 15:21:31 +03:00
Ruslan Talpa 609f1aabca Merge remote-tracking branch 'begriffs/v3' into v3 2015-10-16 11:34:10 +03:00
Joe Nelson d3eca26393 Merge pull request #319 from ruslantalpa/master
avoid ByteString -> Text -> ByteString converstion of the response body
2015-10-15 10:22:30 -07:00
Ruslan Talpa 864c865e52 avoid ByteString -> Text -> ByteString converstion of the response body 2015-10-15 16:20:57 +03:00
Joe Nelson 2c67b8d7ba Merge pull request #316 from begriffs/clearer-errors
Clearer errors
2015-10-13 15:36:35 -07:00
Joe Nelson 6dbb69a828 Ensure that version error message tracks changing requirements 2015-10-12 17:10:57 -07:00
Joe Nelson 3733a84a38 Use type alias for clarity 2015-10-12 17:10:57 -07:00
Joe Nelson e37b2d8c59 Provide detailed logging for any db errors caused internally by postgrest 2015-10-12 17:10:52 -07:00
Joe Nelson 2373a41699 Merge pull request #315 from calebmer/feature/pg-string-settings
Use postgres connection string instead of 5+ options
2015-10-12 16:22:27 -07:00
calebmer 2cbf2af6c7 Fix readme bash syntax 2015-10-11 16:51:52 -04:00
calebmer fdcf074dfd Update readme 2015-10-11 16:50:47 -04:00
calebmer de43ac52c4 Rename connection string metavar 2015-10-11 16:47:09 -04:00
calebmer f6aa93f094 Add details to changelog 2015-10-11 16:46:51 -04:00
calebmer 5feb334191 Use postgres connection string 2015-10-11 16:26:29 -04:00
Joe Nelson 3bd10004a7 Merge pull request #314 from calebmer/remove-versioning
Remove versioning feature
2015-10-11 10:35:34 -07:00
calebmer 02c405ad80 Remove versioning feature 2015-10-11 13:16:59 -04:00
Joe Nelson 3e9f9f300c Merge pull request #309 from ruslantalpa/master
Fix for #302 (supporting compound foreign keys in relations)
2015-10-08 12:21:00 -07:00
Ruslan Talpa 6ba4dc4617 Fix for #302 2015-10-08 11:33:42 +03:00
Joe Nelson 2d0c4fecd8 Merge pull request #307 from calebmer/tolerate-missing-role
Tolerate missing role in user creation
2015-10-07 18:09:04 -07:00
calebmer 8e107e5b24 Tolerate missing role in user creation 2015-10-07 20:01:46 -04:00
Joe Nelson 2f551fea97 Merge pull request #305 from diogob/moves_minimum_pg_version_to_config
Moves minimum pg version to config
2015-10-06 17:58:18 -07:00
Diogo Biazus adfd980a60 Moves minimum pg version to config, eliminates magic constant from main and improves docs. 2015-10-06 20:27:54 -04:00
Joe Nelson f186d6bb33 Merge pull request #299 from fike/master
Added files in debian subdirectory.
2015-10-06 09:25:14 -07:00
Joe Nelson 180d647c70 Merge pull request #303 from diogob/refactor_config
Moves all config related code to PostgREST.Config module and sets default db-pass
2015-10-05 19:13:23 -07:00
Diogo Biazus 3020812f71 Adds basic haddock comments on Config module 2015-10-05 21:14:18 -04:00
Diogo Biazus ed9eea3e8b Imports (<>) from Data.Monoid 2015-10-05 18:53:44 -04:00
Diogo Biazus 346170220e Uses empty password by default 2015-10-05 14:58:53 -04:00
Diogo Biazus 3a1f7938e8 Moves all config related code to PostgREST.Config module and
tweak the code to better encapsulate functionality.
2015-10-05 14:50:35 -04:00
Joe Nelson 9b222fb93d Thank you @ruslantalpa 2015-10-04 10:10:17 -07:00
Joe Nelson 0f1b313d96 Merge pull request #295 from ruslantalpa/master
Extend the capabilities of PostgREST #280
2015-10-04 09:42:32 -07:00
Ruslan Talpa b675276f31 merge new test from @diogob 2015-10-04 07:26:05 +03:00
Ruslan Talpa c014d09072 Merge pull request #1 from diogob/fix_composite_fk_children
Adds a failing spec for child relation (comments) using a composite FK
2015-10-04 07:17:04 +03:00
Diogo Biazus 4dd508a628 Adds a failing spec for child relation (comments) using a composite foreign key (references to users_tasks) 2015-10-03 15:02:02 -04:00
Ruslan Talpa e23a49395b error formatting for parsers and relation 2015-10-02 11:36:56 +03:00
Fernando Ike 80f9685bb9 Fixed year dat in d/copyright 2015-10-01 18:15:08 -03:00
Ruslan Talpa 7754f96af8 Merge remote-tracking branch 'begriffs/master' 2015-10-01 22:22:14 +03:00
Joe Nelson 7d8523a786 Merge pull request #298 from diogob/fix_count_none
Fix count=none when we have filters
2015-10-01 10:26:25 -07:00
Ruslan Talpa e6baafdb8f remove space 2015-10-01 15:28:02 +03:00
Ruslan Talpa c7666c0a67 tests for table relations & bugfix for not detecting child relations of view 2015-10-01 14:20:40 +03:00
Ruslan Talpa 8ff4b4be66 lint fix 2015-10-01 10:36:33 +03:00
Ruslan Talpa ffeb8b1d5f Merge remote-tracking branch 'begriffs/master' 2015-10-01 10:32:30 +03:00
Ruslan Talpa 812135d1e5 cleanup suggested by @begriffs 2015-10-01 10:30:41 +03:00
Joe Nelson 3eb58c511b Merge pull request #300 from diogob/update_stack_37
Updates stackage resolver to 3.7
2015-09-30 22:30:40 -07:00
Diogo Biazus 0df6ea57ae Updates stackage resolver to 3.7 2015-09-30 23:35:44 -04:00
Diogo Biazus f0ec46fd11 Fix count=none when we have filters 2015-09-30 16:50:52 -04:00
Fernando Ike add25ae68e Added files in debian subdirectory.
These files are base to build postgrest Debian package. Additional, it
has TODO-deps.md file with whole task to build postgrest as Debian
official package.
2015-09-30 17:49:11 -03:00
Ruslan Talpa 7f2c39ef94 small cleanup suggested by @begriffs 2015-09-29 19:16:25 +03:00
Ruslan Talpa 94b1d2815c small changes suggested by @diogob 2015-09-29 11:40:33 +03:00
Ruslan Talpa 1da26cac98 trying to make it work with ghc 7.8 (3) 2015-09-28 16:54:34 +03:00
Ruslan Talpa a4a2c8b886 trying to make it work with ghc 7.8 (2) 2015-09-28 16:46:33 +03:00
Ruslan Talpa 68c7f45be1 trying to make it work with ghc 7.8 2015-09-28 16:40:22 +03:00
Ruslan Talpa dbf3d5809b add bifunctors to cabal config 2015-09-28 16:23:57 +03:00
Ruslan Talpa bcf3bf5586 handle many to many relations 2015-09-28 15:53:03 +03:00
Ruslan Talpa edff915f9e delete unused functions 2015-09-28 13:41:45 +03:00
Ruslan Talpa 9342cc8c8a removide duplication from data types (all tests passing) 2015-09-28 12:06:49 +03:00
Ruslan Talpa db13724131 moved string packing to parsers 2015-09-28 10:11:59 +03:00
Ruslan Talpa 380cab6ca3 Merge branch 'master' into skin 2015-09-25 22:45:49 +03:00
Ruslan Talpa bd4bf489dc Merge remote-tracking branch 'begriffs/master' 2015-09-25 22:14:58 +03:00
Joe Nelson 29ae1bdefd Merge pull request #293 from diogob/prefer_count_none
Prefer count none
2015-09-25 10:35:53 -07:00
Diogo Biazus efd6580b26 Adds Prefer count=none to changelog 2015-09-25 13:22:19 -04:00
Diogo Biazus f08512eaa9 Updates optparse-applicative package version constraints 2015-09-25 12:19:24 -04:00
Diogo Biazus e28b2dd00e Implements Prefer count=none header
Uses Maybe for total parameter in contentRangeH
2015-09-25 10:52:44 -04:00
Ruslan Talpa 3d1736e0d3 fix for generating count query 2015-09-25 16:33:25 +03:00
Ruslan Talpa 2fb5c5187a deleted some commented code 2015-09-25 15:28:48 +03:00
Ruslan Talpa ff2c0b63e2 changed identation to 2 spaces to match the rest of the project 2015-09-25 12:04:44 +03:00
Ruslan Talpa c42832f1c5 code cleanup 2015-09-25 11:51:37 +03:00
Ruslan Talpa 770e04c04a remove unused functions in pgstructure 2015-09-25 10:36:19 +03:00
Ruslan Talpa 54d1e4112a detect primary keys for views & use cached info in PUT/PATCH requests 2015-09-25 10:30:19 +03:00
Ruslan Talpa 89fffd1518 Merge branch 'master' into skin 2015-09-25 10:00:18 +03:00
Diogo Biazus 1fbf276474 Adds spec for Prefer header set to count=none 2015-09-24 18:31:36 -04:00
Ruslan Talpa 8783615ebc detect primary keys for views (fix #217) 2015-09-24 23:30:47 +03:00
Ruslan Talpa f546dc3ac8 small note about a bug 2015-09-24 22:06:44 +03:00
Ruslan Talpa f2d6c59bab fixes for failing tests (only 2 failing, yay! :)) 2015-09-24 16:29:20 +03:00
Ruslan Talpa 448a81dff8 first fully functioning build, some tests are failing (limit,order,csv not implemented yet) 2015-09-24 11:45:34 +03:00
Ruslan Talpa fb92b76a1a integrated skin code gor generating Sql Query (only left to execute it) 2015-09-23 14:31:24 +03:00
Ruslan Talpa c0e17c44ba all tests passing after moving table structure detection at load time 2015-09-23 09:38:40 +03:00
Ruslan Talpa 6f55e1d389 fix for one of the failing tests (acl for tables added) 2015-09-22 18:17:24 +03:00
Ruslan Talpa a7b883c922 moved db structure detection at the beginning (2 tests failing) 2015-09-22 16:56:19 +03:00
Ruslan Talpa 6bd6108619 Merge remote-tracking branch 'begriffs/master' 2015-09-07 09:28:31 +03:00
Joe Nelson aee0c2c731 Test that patch requests can set a field to null
Exploring situation mentioned in #233
2015-09-06 17:07:00 -07:00
Joe Nelson b5e76f42a5 Note @ruslantalpa's contribution in changelog 2015-09-06 16:14:53 -07:00
Joe Nelson 35f05f6fe1 Standardize indentation 2015-09-06 16:13:11 -07:00
Joe Nelson cf2f576ec0 Merge pull request #276 from ruslantalpa/master
Support for &select=col1,col2,col3 as suggested in issue #227
2015-09-06 15:24:47 -07:00
Ruslan Talpa 92a1d8c7e3 constrain cast parameters to letters only 2015-09-05 16:54:09 +03:00
Ruslan Talpa cc418d3519 aditional tests for bad casting parameters 2015-09-05 15:31:36 +03:00
Ruslan Talpa 9c8ac2a489 tests for &select= feature and support for casting columns (usefull when extracting subfields from json columns) 2015-09-05 15:05:53 +03:00
Ruslan Talpa 6c16395dfb change fn name from selectT to select 2015-09-04 19:04:39 +03:00
Ruslan Talpa 40d4fb0d75 Support for extracting fields from json columns 2015-09-02 12:41:02 +03:00
Ruslan Talpa b9fec2ce41 Support for &select=col1,col2,col3 as suggested in issue #227 2015-09-02 11:03:34 +03:00
Joe Nelson 593f247abb Expose more heroku config vars 2015-09-01 23:16:30 -07:00
Joe Nelson add63ac25b Keep the dilapidated release script alive 2015-09-01 23:13:47 -07:00
Joe Nelson 1ef8cc5048 Bump patch version 2015-09-01 22:29:13 -07:00
Joe Nelson c5836e0c9e Merge pull request #275 from diogob/fix_all_media_types_in_accept
Fix */* in accept headers
2015-09-01 13:10:02 -07:00
Diogo Biazus 922aa702a2 Adds fix to changelog 2015-09-01 15:58:07 -04:00
Diogo Biazus 89c581816e Adds */* as a valid media type that will return json [fix #274] 2015-09-01 15:57:59 -04:00
Joe Nelson 3d670b9c03 bump minor version 2015-08-28 19:19:37 -07:00
Joe Nelson 5bf644867f Merge pull request #271 from begriffs/surprise-404
Allow continued auth access after db errors
2015-08-26 20:58:55 -07:00
Joe Nelson fcdae73f49 Note fix in changelog 2015-08-26 20:49:23 -07:00
Joe Nelson 1656fb9f57 Let the transaction reset the role and user id for us 2015-08-26 20:49:23 -07:00
Joe Nelson 594327924c Set role locally in a tx to ensure it is reset after error 2015-08-26 20:49:23 -07:00
Joe Nelson 480800edbd Problem after exceptions when authed
Reproduces #264
2015-08-26 20:49:23 -07:00
Joe Nelson 9b01d1b1ab Helpful directions in contributing doc 2015-08-26 20:47:22 -07:00
Joe Nelson ed810bc380 Operator negation 2015-08-21 19:35:48 -07:00
Joe Nelson 789db9a017 Merge pull request #266 from diogob/adds_not_unary_operator
Adds not as a keyword that can optionally be prepended to any operator in a parameter value [fix #173]
2015-08-21 19:25:02 -07:00
Diogo Biazus 52626cc86d Adds test cases for not operator in equality, inequality, like, ilike, tesarch (@@) and is null queries 2015-08-21 15:08:52 -04:00
Diogo Biazus 32b97ef076 Adds not as a keyword that can optionally be prepended to any operator in a parameter value 2015-08-21 10:33:41 -04:00
Joe Nelson 8e319321c9 RPC and Stack 2015-08-20 22:37:19 -07:00
Joe Nelson cdac6d385c Merge pull request #228 from begriffs/rpc
Expose stored procedures
2015-08-20 22:27:30 -07:00
Joe Nelson d65d011e5e Tests for rpc 2015-08-20 22:14:09 -07:00
Joe Nelson 28f771324d Nest response JSON more shallowly 2015-08-20 22:14:08 -07:00
Joe Nelson cf04fbd6ea Call procedures that return setof, not just text
The output is too deeply nested however
2015-08-20 21:52:46 -07:00
Joe Nelson bb511f0df2 WIP: call stored pprocedures that emit plain text
The beginning of #114
2015-08-20 21:52:46 -07:00
Joe Nelson f564fb0977 Rename QualifiedTable to encompass proc names as well 2015-08-20 21:35:12 -07:00
Diogo BiazusandJoe Nelson 3b017dfdf6 Adds 415 response for any non-empty Accept header different from application/json or text/csv. Uses apropriate Content-Type header when sending CSV format. 2015-08-20 20:47:36 -07:00
Diogo BiazusandJoe Nelson cb7d00b839 Adds tags file to gitignore 2015-08-20 20:47:36 -07:00
Joe Nelson 010e18ea0b Merge pull request #269 from diogob/build_with_stack
Build with stack
2015-08-20 15:29:10 -07:00
Diogo Biazus c34f96ce2e Removes body matcher to compile and test against any aeson version >= 0.8 2015-08-20 16:43:57 -04:00
Diogo Biazus f9d50018d9 Rollback to aeson 0.8.0.2 to allow building with stackage, and adds stack.yml 2015-08-20 15:50:28 -04:00
Diogo Biazus 6c1233fcec Adss stack-work directory to gitignore 2015-08-20 15:15:53 -04:00
Joe Nelson acd8e92d24 Note the NOT IN addition 2015-08-17 09:40:49 -07:00
Joe Nelson 559d370a89 Merge pull request #263 from rall/notin
NOT IN queries
2015-08-17 09:38:45 -07:00
Richard Allaway 4cf51b7005 fixes spec for changed error message from updated aeson library 2015-08-17 11:10:15 -04:00
Richard Allaway 4e77492797 adds a spec for NOT IN query case 2015-08-17 10:24:51 -04:00
Richard Allaway f16e2e3ee5 adds 'not in' query 2015-08-17 10:24:51 -04:00
Joe Nelson ebc8c387e0 Thanks @diogob! 2015-08-15 12:14:20 -07:00
Joe Nelson 734484714c CSV responses! 2015-08-15 11:59:12 -07:00
Joe Nelson adac39bd7c Use Content-Type text/csv for CSV responses 2015-08-15 11:57:57 -07:00
Diogo Biazus 9d5011e864 Implements CSV resnponse for the appropriate accept headers 2015-08-14 11:04:26 -04:00
Diogo Biazus ca40ba1fda Refactors app function to DRY header lookups 2015-08-14 11:04:26 -04:00
Joe Nelson 894455f2cd Relax hasql deps for packdeps checker 2015-08-12 00:05:38 -07:00
Joe Nelson 6cb73062a9 Better shields 2015-08-11 23:18:47 -07:00
Joe Nelson 86c68d191c Updated maintenance note in contributing doc 2015-08-09 14:04:57 -07:00
Joe Nelson d20c252cb3 Adjust version of hasql-postgres for packdeps 2015-08-08 12:57:44 -07:00
Joe Nelson cd6b688f7f Merge pull request #244 from diogob/adds_materialized_views_to_root
Adds materialized views to list of relations in GET / [#242]
2015-08-01 16:16:23 -07:00
Joe Nelson 49f41d8edb Merge pull request #243 from diogob/fix_count_column_name_case
Fixes error code 42803 when trying to query a view with a column named count
2015-08-01 16:15:02 -07:00
Diogo Biazus b45953dff8 Mentions fix in CHANGELOG 2015-08-01 01:23:31 -04:00
Diogo Biazus 8075d7e51a Mentions fix in CHANGELOG 2015-08-01 01:21:42 -04:00
Diogo Biazus ab0170ffaf Adds materialized views to list of relations in GET / [#242] 2015-08-01 01:09:12 -04:00
Diogo Biazus 449cacdacf Fixes error code 42803 when trying to query a view with a column named count. 2015-08-01 00:15:17 -04:00
Joe Nelson e724c2df00 Merge pull request #230 from edelans/master
Add link to Jonathan Harrington's nice tutorial
2015-07-26 10:17:39 -07:00
Joe Nelson 12dc180065 Merge pull request #237 from datasaur/master
Enable log capture if stdout is not a terminal (issue #229)
2015-07-24 09:51:37 -07:00
MattK 8eae978eae Enable log capture if stdout is not a terminal 2015-07-24 12:14:18 -04:00
Edouard de Lansalut 019d53bca1 Add link to Jonathan Harrington's nice tutorial 2015-07-22 09:47:14 +02:00
Joe Nelson e1d7dc3dea Note computed columns in changelog 2015-07-21 22:34:45 -07:00
Joe Nelson 28a2826fa8 Merge pull request #221 from diogob/allow_virtual_fields_in_where
Qualifies columns of WHERE clauses so we can use computed columns as filters
2015-07-21 22:31:10 -07:00
Diogo Biazus c76864a653 Qualifies columns used in WHERE clauses so we can use computed columns as filters 2015-07-10 12:35:24 -04:00
Joe Nelson c0d44232a5 Note Debian changes in changelog 2015-07-09 22:52:14 -07:00
Joe Nelson e34e92eb44 Merge pull request #216 from mkhon/master
Debian init script for postgrest.
2015-07-09 22:49:11 -07:00
Joe Nelson 956f73d997 Regression test for situation reported in issue #203 2015-07-03 22:51:04 -07:00
Joe Nelson df04d26c15 Merge pull request #219 from diogob/check_postgresql_version
Verifies PostgreSQL version is supported (+9.2) before spawning server
2015-07-02 13:21:29 -07:00
Diogo Biazus 9c69553373 Verifies PostgreSQL version is supported (+9.2) before spawning server [fixes #157] 2015-07-02 09:26:58 -04:00
Joe Nelson 6604293ac1 Remove regex-tdfa-text to allow building in GHC 7.10
Fixes #212
2015-07-01 21:55:35 -07:00
Max Khon 60a61adbce Use POSTGREST_USER. 2015-06-24 18:30:56 +06:00
Max Khon b56ab47f84 Debian init script for postgrest. 2015-06-24 18:07:30 +06:00
Joe Nelson 25492a089b Merge pull request #214 from diogob/refactor_to_bool
Removes toBool function as we now cast the 'YES/NO' values to boolean in PostgreSQL's queries
2015-06-22 22:29:07 -07:00
Diogo Biazus e87be593c0 Removes toBool function as we now cast the 'YES/NO' values to boolean in PostgreSQL's queries 2015-06-22 18:09:45 -04:00
Joe Nelson 1937363fc8 Note @diogob's contribution 2015-06-21 22:18:51 -07:00
Joe Nelson d68cbec25c Merge pull request #209 from diogob/insertable_views_with_triggers
Changes the insertable to true in views that are insertable through triggers [fixes #206]
2015-06-21 22:16:43 -07:00
Diogo Biazus f4011e5d8c Changes the insertable to true in views that are insertable through triggers [fixes #206] 2015-06-22 00:16:23 -04:00
Joe Nelson e93c96a6f8 Link to Heroku troubleshooting Wiki in readme 2015-06-21 14:01:42 -07:00
Joe Nelson 070f67e9c6 Merge pull request #213 from begriffs/packdeps
Loosen dependency version constraints for packdeps
2015-06-20 12:16:05 -07:00
Joe Nelson 61dac3b02b Loosen dependency version constraints for packdeps 2015-06-20 12:12:36 -07:00
Joe Nelson 169157ec6d Thanks @framp 2015-06-17 20:24:10 -07:00
Joe Nelson bd304c9fc3 Bump version 2015-06-03 21:22:17 -07:00
Joe Nelson 3989aaa144 Note @framp's auth id change 2015-05-26 14:58:59 -07:00
Joe Nelson 1d51a5f543 Merge pull request #201 from framp/master
User_id support (via user_vars)
2015-05-26 14:55:56 -07:00
Joe Nelson e4dafad64d Use github release feature to host binaries 2015-05-25 21:56:47 -07:00
Joe Nelson 3f31c60f1d Mention JWT in readme 2015-05-25 21:46:05 -07:00
Federico Rampazzo 83f48dcd15 User_id support (via user_vars) 2015-05-24 03:26:42 +01:00
Joe Nelson 1cc53245c5 Full text search in changelog 2015-05-23 08:21:02 -07:00
Joe Nelson f24ba048af Merge pull request #199 from diogob/tsearch_operator
Tsearch operator
2015-05-23 08:15:44 -07:00
Diogo Biazus a980db6d2d Adds spec for @@ operator and implements it in PgQuery 2015-05-22 12:28:26 -04:00
Diogo Biazus 4277284a69 Adds table tsearch to test for @@ operator against a tsvector field. Fixes StructureSpec accordingly. 2015-05-22 12:15:14 -04:00
Joe Nelson b070994912 Patch version bump for conditional -Werror flag 2015-05-21 00:18:26 -07:00
Joe Nelson 988df54e53 Use Werror on CI 2015-05-21 00:18:03 -07:00
Joe Nelson 4bbf053896 Bump version 2015-05-20 09:42:51 -07:00
Joe Nelson da79f1da3e Merge pull request #193 from srid/makelibrary
Make postgrest a library
2015-05-19 23:59:16 -07:00
Sridhar Ratnakumar c3ad87ffaf Make postgrest usable as a library 2015-05-19 23:40:39 -07:00
Joe Nelson 035acebf59 Update CHANGELOG.md 2015-05-19 22:23:49 -07:00
Joe Nelson 0b665676d7 Update CHANGELOG.md 2015-05-19 22:21:28 -07:00
Joe Nelson dcf62b020f Merge pull request #194 from framp/jwt
JWT support
2015-05-19 22:17:34 -07:00
Federico Rampazzo 77aecc9e86 JWT support 2015-05-20 06:01:50 +01:00
Joe Nelson e35ad0f340 Update changelog, remove debugging 2015-05-12 10:55:51 -07:00
Joe Nelson 709e70561f Provide more information in PATCH response
* 404 if no records updated
* Range header for number updated
* Full results depending on Prefer header

Fixes #187
Fixes #182
2015-05-12 10:47:55 -07:00
Joe Nelson ef0dc26de5 Merge pull request #186 from jcristovao/patch-2
GHC 7.10.1 support
2015-04-24 09:37:27 -07:00
João Cristóvão 35d36d95c1 GHC 7.10.1 support
Without it, the following error occurs:

```
src/PgStructure.hs:88:5:
    Non type-variable argument
      in the constraint: Data.String.Conversions.ConvertibleStrings
                           Text k
    (Use FlexibleContexts to permit this)
    When checking that ‘addFK’ has the inferred type
      addFK :: forall k.
               (Ord k, Data.String.Conversions.ConvertibleStrings Text k) =>
               Map.Map k ForeignKey -> Column -> Column
    In an equation for ‘columns’:
        columns table
          = do { cols <- H.listEx
                         $ (\ _1 _2
                              -> Hasql.Backend.Stmt
                                   "select info.table_schema as schema, info.table_name as table_name, info.column_name as name, info.ordinal_position as position, info.is_nullable as nullable, info.data_type as col_type, info.is_updatable as updatable, info.character_maximum_length as max_len, info.numeric_precision as precision, info.column_default as default_value, array_to_string(enum_info.vals, ',') as enum from ( select table_schema, table_name, column_name, ordinal_position, is_nullable, data_type, is_updatable, character_maximum_length, numeric_precision, column_default, udt_name from information_schema.columns where table_schema = ? and table_name = ? ) as info left outer join ( select n.nspname as s, t.typname as n, array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals from pg_type t join pg_enum e on t.oid = e.enumtypid join pg_catalog.pg_namespace n ON n.oid = t.typnamespace group by s, n ) as enum_info on (info.udt_name = enum_info.n) order by position"
                                   (GHC.ST.runST (do { ... }))
                                   True)
                             (qtSchema table) (qtName table);
                 fks <- foreignKeys table;
                 return $ map (addFK fks . columnFromRow) cols }
          where
              addFK fks col = col {colFK = Map.lookup (cs . colName $ col) fks}
```

Also, the regex-tdfa-text library needs a similar patch, but I didn't have time to contact the author yet.

Cheers
2015-04-24 16:22:43 +01:00
Joe Nelson 13cda09c7e Contributing 2015-04-22 17:22:39 -07:00
Joe Nelson 5688030104 Allow posting JSON array and object
Fixes #168

Fixes #156
2015-04-19 17:46:11 -07:00
Joe Nelson 02228b76cc Bump version 2015-04-17 16:26:27 -07:00
Joe Nelson 4d7cc3d67e Merge branch 'bulk-insert' 2015-04-17 15:55:42 -07:00
Joe Nelson ad8700e996 bulk insert in changelog 2015-04-17 15:52:57 -07:00
Joe Nelson fbc90bdb84 Tests for csv bulk import
Fixes #17
2015-04-17 15:42:58 -07:00
Joe Nelson f4c49f03f4 Allow NULL in csv field and unquote the multipart headers 2015-04-16 12:43:22 -07:00
Joe Nelson a87f13f9bb Fix original tests 2015-04-16 10:45:24 -07:00
Joe Nelson 7d03a71fed All tests but one are passing 2015-04-12 19:56:33 -07:00
Joe Nelson 70d33445db WIP: nice but doomed approach to making multipart response 2015-04-12 13:03:16 -07:00
Joe Nelson 4dbcf45555 WIP: typechecking but not yet sending back links
Rather amazing how well this actually works given
it only type checked
2015-04-11 21:41:32 -07:00
Joe Nelson 87acee924e WIP: parsing csv 2015-04-05 17:13:16 -07:00
Joe Nelson a22cf82688 Build sql for inserting multiple rows 2015-04-04 20:20:13 -07:00
Joe Nelson 532cfdff95 Merge pull request #183 from begriffs/jsonb
Filter results using properties in a jsonb column
2015-04-04 18:35:40 -07:00
Joe Nelson 5807b41997 Add jsonb to changelog 2015-04-04 18:33:08 -07:00
Joe Nelson c78d323989 Specify hasql versions exactly to help ci 2015-04-04 17:43:14 -07:00
Joe Nelson 9792b9b46a Allow filtering by values inside json columns 2015-04-04 17:38:54 -07:00
Joe Nelson ca1c524ede WIP: working on querying and ordering with jsonb paths
Affects #118
2015-03-30 00:51:52 -07:00
Joe Nelson a1822a8e08 Update whitespace 2015-03-29 12:32:14 -07:00
Joe Nelson 066cdbc697 Merge pull request #176 from brikou/patch-2
Add link to GUI demo
2015-03-23 18:35:02 -07:00
Brikou CARRE b9c3902bd8 Add link to GUI demo
You can read more about using ng-admin with postgREST here http://marmelab.com/blog/2015/03/23/using-ng-admin-with-postgrest.html
2015-03-23 15:54:34 +01:00
Joe Nelson 03613e2f8e Add link to tutorial
Thanks @prio
2015-03-22 20:27:54 -07:00
Joe Nelson de7eecb166 Document fix from pull request 2015-03-16 21:11:57 -07:00
Joe Nelson 417d98d7fa Merge pull request #172 from GaloisInc/master
Show long help output with defaults on usage errors
2015-03-16 21:09:57 -07:00
Jonathan Daugherty 6dfbe854a0 Make postgrest show long help output with defaults on usage errors
This makes optparse-applicative show the long version of help output.
Its default behavior is not to do this since the showHelpOnError
setting defaults to False.  This turns it on.
2015-03-16 13:27:22 -07:00
Joe Nelson a9bc119b82 Merge branch 'insert-nulls'
Fixes #166
2015-03-15 18:30:47 -07:00
Joe Nelson 42d3d0de6c Fix location header for inserted objects with nulls 2015-03-15 18:26:32 -07:00
Joe Nelson ddb5ba8b64 Can now post nulls, but header link is wrong
affects #166
2015-03-15 16:30:42 -07:00
Joe Nelson 185d5b1c62 Merge branch 'jcristovao-isnull' 2015-03-15 15:22:28 -07:00
Joe Nelson f7ff08edf7 Test is.null matcher 2015-03-15 15:20:31 -07:00
João CristóvãoandJoe Nelson f4e0c12cba IS/IS NOT null, true, false 2015-03-15 13:37:45 -07:00
Joe Nelson 8ed599a769 Merge branch 'jcristovao-nullsFirst' 2015-03-15 12:30:34 -07:00
Joe Nelson aeb62e75bd Add nullsfirst to changelog 2015-03-15 12:30:17 -07:00
Joe Nelson 5c7ee1effc Fix problems caused by my rebase 2015-03-15 12:24:30 -07:00
João CristóvãoandJoe Nelson 01c67ab793 Added tests for nullsfirst / nullslast 2015-03-15 11:37:06 -07:00
João CristóvãoandJoe Nelson 97c9bfe93f Added support for nulls first / nulls last 2015-03-15 10:28:21 -07:00
Joe Nelson 6e6769ea16 Bump version 2015-03-03 23:29:22 -08:00
Joe Nelson 1e417aef90 Note schema override in changelog 2015-03-03 23:20:13 -08:00
Joe Nelson 60eb826cb0 Merge branch 'v1-schema-override'
Affects #155
Affects #158
Fixes #117
2015-03-03 23:18:59 -08:00
Joe Nelson 3d3c7277c7 Allow user to override schema used for v1 of api 2015-03-03 22:29:37 -08:00
Joe Nelson 5be0b5d5ae Return inserted object from POST when Prefer: return=representation
Fixes #27
Fixes #159
2015-03-01 21:42:05 -08:00
Joe Nelson 11e5918690 Merge branch 'select-in'
Fixes #98
Affects #158
2015-03-01 19:18:21 -08:00
Joe Nelson c207e7ee30 Add IN filter to changelog 2015-03-01 18:38:36 -08:00
Joe Nelson 6474a83221 Support IN query param operator 2015-03-01 18:30:08 -08:00
Joe Nelson 9b15071f23 Log requests
Fixes #141
2015-02-28 09:31:00 -08:00
Joe Nelson 51b379d78b Clarify the --secure command line option 2015-02-19 21:28:07 -08:00
Joe Nelson cac5daca01 Merge pull request #154 from brikou/patch-1
Fix URL to video
2015-02-19 10:30:20 -08:00
Brikou CARRE 4bc66c4ca3 Fix URL to video 2015-02-19 10:04:45 +01:00
86 changed files with 8563 additions and 1952 deletions
+4
View File
@@ -1,3 +1,4 @@
.DS_Store
db
dist
.cabal-sandbox
@@ -5,3 +6,6 @@ cabal.sandbox.config
hscope.out
codex.tags
.anvil
.stack-work
tags
site
+160
View File
@@ -3,6 +3,166 @@
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
- Fix bug in many-many relation detection - @ruslantalpa
- Inconsistent escaping of table names in read queries - @calebmer
## [0.3.0.2] - 2015-12-16
### Fixed
- Miscalculation of time used for expiring tokens - @calebmer
- Remove bcrypt dependency to fix Windows build - @begriffs
- Detect relations event when authenticator does not have rights to intermediate tables - @ruslantalpa
- Ensure db connections released on sigint - @begriffs
- Fix #396 include records with missing parents - @ruslantalpa
- `pgFmtIdent` always quotes #388 - @calebmer
- Default schema, changed from `"1"` to `public` - @calebmer
- #414 revert to separate count query - @ruslantalpa
- Fix #399, allow inserting in tables with no select privileges using "Prefer: representation=minimal" - @ruslantalpa
### Added
- Allow order by computed columns - @diogob
- Set max rows in response with --max-rows - @begriffs
- Selection by column name (can detect if `_id` is not included) - @calebmer
## [0.3.0.1] - 2015-11-27
### Fixed
- Filter columns on embedded parent items - @ruslantalpa
## [0.3.0.0] - 2015-11-24
### Fixed
- Use reasonable amount of memory during bulk inserts - @begriffs
### Added
- Ensure JWT expires - @calebmer
- Postgres connection string argument - @calebmer
- Encode JWT for procs that return type `jwt_claims` - @diogob
- Full text operators `@>`,`<@` - @ruslantalpa
- Shaping of the response body (filter columns, embed relations) with &select parameter for POST/PATCH - @ruslantalpa
- Detect relationships between public views and private tables - @calebmer
- `Prefer: plurality=singular` for selecting single objects - @calebmer
### Removed
- API versioning feature - @calebmer
- `--db-x` command line arguments - @calebmer
- Secure flag - @calebmer
- PUT request handling - @ruslantalpa
### Changed
- Embed foreign keys with {} rather than () - @begriffs
- Remove version number from binary filename in release - @begriffs
## [0.2.12.1] - 2015-11-12
### Fixed
- Correct order for -> and ->> in a json path - @ruslantalpa
- Return empty array instead of 500 when a set returning function returns an empty result set - @diogob
## [0.2.12.0] - 2015-10-25
### Added
- Embed associations, e.g. `/film?select=*,director(*)` - @ruslantalpa
- Filter columns, e.g. `?select=col1,col2` - @ruslantalpa
- Does not execute the count total if header "Prefer: count=none" - @diogob
### Fixed
- Tolerate a missing role in user creation - @calebmer
- Avoid unnecessary text re-encoding - @ruslantalpa
## [0.2.11.1] - 2015-09-01
### Fixed
- Accepts `*/*` in Accept header - @diogob
## [0.2.11.0] - 2015-08-28
### Added
- Negate any filter in a uniform way, e.g. `?col=not.eq=foo` - @diogob
- Call stored procedures
- Filter NOT IN values, e.g. `?col=notin.1,2,3` - @rall
- CSV responses to GET requests with `Accept: text/csv` - @diogob
- Debian init scripts - @mkhon
- Allow filters by computed columns - @diogob
### Fixed
- Reset user role on error
- Compatible with Stack
- Add materialized views to results in GET / - @diogob
- Indicate insertable=true for views that are insertable through triggers - @diogob
- Builds under GHC 7.10
- Allow the use of columns named "count" in relations queried - @diogob
## [0.2.10.0] - 2015-06-03
### Added
- Full text search, eg `/foo?text_vector=@@.bar`
- Include auth id as well as db role to views (for row-level security)
## [0.2.9.1] - 2015-05-20
### Fixed
- Put -Werror behind a cabal flag (for CI) so Hackage accepts package
## [0.2.9.0] - 2015-05-20
### Added
- Return range headers in PATCH
- Return PATCHed resources if header "Prefer: return=representation"
- Allow nested objects and arrays in JSON post for jsonb columns
- JSON Web Tokens - [Federico Rampazzo](https://github.com/framp)
- Expose PostgREST as a Haskell package
### Fixed
- Return 404 if no records updated by PATCH
## [0.2.8.0] - 2015-04-17
### Added
- Option to specify nulls first or last, eg `/people?order=age.desc.nullsfirst`
- Filter nulls, `?col=is.null` and `?col=isnot.null`
- Filter within jsonb, `?col->a->>b=eq.c`
- Accept CSV in post body for bulk inserts
### Fixed
- Allow NULL values in posts
- Show full command line usage on param errors
## [0.2.7.0] - 2015-03-03
### Added
- Server response logging
- Filter IN values, e.g. `?col=in.1,2,3`
- Return POSTed resource if header "Prefer: return=representation"
- Allow override of default (v1) schema
## [0.2.6.0] - 2015-02-18
### Added
- A changelog
+59
View File
@@ -0,0 +1,59 @@
# Contributing to PostgREST
**First:** if you're unsure or afraid of _anything_, just ask or
submit the issue or pull request anyways. You won't be yelled at
for giving your best effort. The worst that can happen is that
you'll be politely asked to change something. We appreciate any
sort of contributions, and don't want a wall of rules to get in the
way of that.
However, for those individuals who want a bit more guidance on the
best way to contribute to the project, read on. This document will
cover what we're looking for. By addressing all the points we're
looking for, it raises the chances we can quickly merge or address
your contributions.
## Issues
### Reporting an Issue
* Make sure you test against the latest released version. It is possible
we already fixed the bug you're experiencing.
* Also check the `CHANGELOG.md` to see if any unreleased changes affect
the issue. The very newest changes can take a little while to be released
as a new official version.
* Provide steps to reproduce the issue, including your OS version and
the specific database schema that you are using.
* Please include SQL logs for issues involving runtime problems. To obtain logs first
[enable logging all statements](http://www.microhowto.info/howto/log_all_queries_to_a_postgresql_server.html),
then [find your logs](http://blog.endpoint.com/2014/11/dear-postgresql-where-are-my-logs.html).
## Code
### Haskell Conventions
* All contributions must pass the tests before being merged. When
you create a pull request your code will automatically be tested.
* All code must also pass [hlint](http://community.haskell.org/~ndm/hlint/)
with no warnings. This helps enforce a uniform style for all
committers. Continuous integration will check this as well on every
pull request.
* For help building the Haskell code on your computer check out the [building from
source](https://github.com/begriffs/postgrest/wiki/Building-from-source)
wiki page.
## Maintenance
### Schedule
Currently I (@begriffs) am the sole maintainer, and while I am
overjoyed to help resolve issues I also have to balance this with
my other obligations. If you don't get a response right away
don't worry, I will definitely get to it. Also you can join the
Gitter [chat room](https://gitter.im/begriffs/postgrest) to
discuss issues you are having.
+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
+66 -54
View File
@@ -1,15 +1,17 @@
![Logo](static/logo.png "Logo")
[![Build Status](https://circleci.com/gh/begriffs/postgrest.png?circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master)
[![Build Status](https://circleci.com/gh/begriffs/postgrest.png?style=shield&circle-token=f723c01686abf0364de1e2eaae5aff1f68bd3ff2)](https://circleci.com/gh/begriffs/postgrest/tree/master)
<a href="https://heroku.com/deploy?template=https://github.com/begriffs/postgrest">
<img src="static/heroku.png" alt="Deploy">
<img src="https://img.shields.io/badge/%E2%86%91_Deploy_to-Heroku-7056bf.svg" alt="Deploy">
</a>
[![Join the chat at https://gitter.im/begriffs/postgrest](https://img.shields.io/badge/gitter-join%20chat%20%E2%86%92-brightgreen.svg)](https://gitter.im/begriffs/postgrest)
PostgREST serves a fully RESTful API from any existing PostgreSQL
database. It provides a cleaner, more standards-compliant, faster
API than you are likely to write from scratch.
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Watch [Video](https://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
### Demo [postgrest.herokuapp.com](https://postgrest.herokuapp.com) | Read [Docs](http://postgrest.com/) | Watch [Video](http://begriffs.com/posts/2014-12-30-intro-to-postgrest.html)
Try making requests to the live demo server with an HTTP client
such as [postman](http://www.getpostman.com/). The structure of the
@@ -18,28 +20,39 @@ demo database is defined by
You can use it as inspiration for test-driven server migrations in
your own projects.
Also try other tools in the PostgREST
[ecosystem](http://postgrest.com/install/ecosystem/) like the
[ng-admin demo](http://marmelab.com/ng-admin-postgrest).
### Usage
Download the binary ([OS X](http://bin.begriffs.com/dbapi/osx/postgrest-0.2.6.0.tar.xz) / [Linux](http://bin.begriffs.com/dbapi/heroku/postgrest-0.2.6.0.tar.xz)) and invoke like so:
1. Download the binary ([latest release](https://github.com/begriffs/postgrest/releases/latest))
for your platform.
2. Invoke like so:
```bash
postgrest --db-host localhost --db-port 5432 \
--db-name my_db --db-user postgres \
--db-pass foobar --db-pool 200 \
--anonymous postgres --secure \
--port 3000
```
```bash
postgrest postgres://postgres:foobar@localhost:5432/my_db \
--port 3000 \
--schema public \
--anonymous postgres \
--pool 200
```
For more information on valid connection strings see the
[PostgreSQL docs](http://www.postgresql.org/docs/9.4/static/libpq-connect.html#LIBPQ-CONNSTRING).
### Performance
TLDR; subsecond response times for up to 2000 requests/sec on Heroku free tier. ([see the load test](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling))
TLDR; subsecond response times for up to 2000 requests/sec on Heroku
free tier. ([see the load
test](http://postgrest.com/admin/performance/#benchmarks))
If you're used to servers written in interpreted languages (or named
after precious gems), prepare to be pleasantly surprised by PostgREST
performance.
Three factors contribute to the speed. First the server is written
in [Haskell](https://new-www.haskell.org/) using the
in [Haskell](https://www.haskell.org/) using the
[Warp](http://www.yesodweb.com/blog/2011/03/preliminary-warp-cross-language-benchmarks)
HTTP server (aka a compiled language with lightweight threads).
Next it delegates as much calculation as possible to the database
@@ -57,58 +70,64 @@ by
* Reusing prepared statements
* Keeping a pool of db connections
* Using the Postgres binary protocol
* Using the PostgreSQL binary protocol
* Being stateless to allow horizontal scaling
Ultimately the server (when load balanced) is constrained by database
performance. This may make it inappropriate for very large traffic
load. To learn more about scaling with Heroku and Amazon RDS see
the [performance guide](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling).
the [performance guide](http://postgrest.com/admin/performance/).
Alternatively [CitusDB](https://www.citusdata.com/products/what-is-citusdb)
supports Postgres clustering for higher performance.
Other optimizations are possible, and some are outlined in the
[Future Features](#future-features).
### Security
PostgREST handles authentication (HTTP Basic over SSL) and delegates
authorization to the role information defined in the database. This
ensures there is a single declarative source of truth for security.
When dealing with the database the server assumes the identity of
the currently authenticated user, and for the duration of the
connection cannot do anything the user themselves couldn't.
PostgREST handles authentication (via [JSON Web
Tokens](http://postgrest.com/admin/security/#json-web-tokens))
and delegates authorization to the role information defined in the
database. This ensures there is a single declarative source of truth
for security. When dealing with the database the server assumes
the identity of the currently authenticated user, and for the
duration of the connection cannot do anything the user themselves
couldn't. Other forms of authentication can be built on top
of the JWT primitive. See the docs for more information.
Postgres 9.5 will soon support true [row-level
security](http://michael.otacoo.com/postgresql-2/postgres-9-5-feature-highlight-row-level-security/).
In the meantime what isn't yet implemented can be simulated with
triggers and security-barrier views. Because the possible queries
to the database are limited to certain templates using
PostgreSQL 9.5 supports true [row-level
security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html).
In previous versions it can be simulated with triggers and
security-barrier views. Because the possible queries to the database
are limited to certain templates using
[leakproof](http://blog.2ndquadrant.com/how-do-postgresql-security_barrier-views-work/)
functions, the trigger workaround does not compromise row-level
security.
For example security patterns see the [security
guide](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions).
guide](http://postgrest.com/admin/security/).
### Versioning
A robust long-lived API needs the freedom to exist in multiple
versions. PostgREST supports versioning through HTTP content
negotiation. Requests for a certain version translate into switching
which database schema to search for tables. PostgreSQL schema search
paths allow tables from earlier versions to be reused verbatim in
later versions.
versions. PostgREST does versioning through database schemas. This
allows you to expose tables and views without making the app brittle.
Underlying tables can be superseded and hidden behind public facing
views. You run an instance of PostgREST per schema and route requests
among them with a reverse proxy such as [nginx](http://nginx.org).
Learn more [here](http://postgrest.com/admin/versioning/).
To learn more, see the [guide to versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning).
### Self-documention
### Self-documentation
Rather than writing and maintaining separate docs yourself let the
API explain its own affordances using HTTP. All PostgREST endpoints
respond to the OPTIONS verb and explain what they support as well
as the data format of their JSON payload.
as the data format of their JSON payload. RAML support is an upcoming
feature.
The number of rows returned by an endpoint is reported by - and
limited with - range headers. More about
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).
There are more opportunities for self-documentation listed in [Future
@@ -124,9 +143,9 @@ data (including your API server).
The PostgREST exposes HTTP interface with safeguards to prevent
surprises, such as enforcing idempotent PUT requests, and
See examples of [Postgres
See examples of [PostgreSQL
constraints](http://www.tutorialspoint.com/postgresql/postgresql_constraints.htm)
and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
and the [guide to routing](http://postgrest.com/api/reading/).
### Future Features
@@ -139,21 +158,14 @@ and the [guide to routing](https://github.com/begriffs/postgrest/wiki/Routing).
* Describe more relationships with Link headers
* Depending on accept headers, render OPTIONS as [RAML](http://raml.org/) or a
relational diagram
* Add two-legged auth with OAuth 1.0a(?)
* ... the other [issues](https://github.com/begriffs/postgrest/issues)
### Guides
* [Routing](https://github.com/begriffs/postgrest/wiki/Routing)
* [Versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning)
* [Performance](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling)
* [Security](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions)
### Thanks
* [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions
* [Nikita Volkov](https://github.com/nikita-volkov) for writing the
wonderful [Hasql](https://github.com/nikita-volkov/hasql) library
and helping me use it
* [Mikey Casalaina](https://github.com/casalaina) for the cool logo
I'm grateful to the generous project
[contributors](https://github.com/begriffs/postgrest/graphs/contributors)
who have improved PostgREST immensely with their code and good
judgement. See more details in the
[changelog](https://github.com/begriffs/postgrest/blob/master/CHANGELOG.md).
The cool logo came from [Mikey Casalaina](https://github.com/casalaina).
+11 -1
View File
@@ -10,7 +10,7 @@
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.2.6.0"
"value": "0.3.1.1"
},
"DB_NAME": {
"description": "Database name",
@@ -41,6 +41,16 @@
"description": "Maximum number of connections in database pool",
"required": false,
"value": "10"
},
"JWT_SECRET": {
"description": "Secret used to encrypt JSON Web Tokens",
"required": false,
"value": "secret"
},
"SCHEMA": {
"description": "DB schema to be exported",
"required": false,
"value": "1"
}
}
}
+22 -6
View File
@@ -1,10 +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.8.3
override:
- 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
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
+52
View File
@@ -0,0 +1,52 @@
# TODO list to build debian "official" package
It feels for free to modify, fix or take some task or all.
## debian/control
* Fill description field
* Add Vcs-Browser
* Add Vcs-Git
* Add Uploaders field
## debian/copyright
* Add more contributers
## Dependencies packages
Some libraries dependencies aren't Debian package. Below is the list was built by [cabal-debian](https://wiki.debian.org/Haskell/CollabMaint/GettingStarted). These libraries are necessary to build Postgrest the right way.
* libghc-base64-string-dev
* libghc-base64-string-prof
* libghc-bcrypt-dev
* libghc-bcrypt-prof
* libghc-hasql-dev
* libghc-hasql-prof
* libghc-hasql-backend-dev
* libghc-hasql-backend-prof
* libghc-hasql-postgres-dev
* libghc-hasql-postgres-prof
* libghc-string-conversions-dev
* libghc-string-conversions-prof
* libghc-wai-cors-dev
* libghc-wai-cors-prof
* libghc-wai-middleware-static-dev
* libghc-wai-middleware-static-prof
* libghc-hasql-dev
* libghc-hasql-backend-dev
* libghc-hasql-postgres-dev
* libghc-heredoc-dev
* libghc-hspec-wai-dev
* libghc-hspec-wai-json-dev
* libghc-http-media-dev
* libghc-packdeps-dev
* libghc-base64-string-doc
* libghc-bcrypt-doc
* libghc-hasql-doc
* libghc-hasql-backend-doc
* libghc-hasql-postgres-doc
* libghc-string-conversions-doc
* libghc-wai-cors-doc
* libghc-wai-middleware-static-doc
+5
View File
@@ -0,0 +1,5 @@
haskell-postgrest (0.2.11.1-1) UNRELEASED; urgency=low
* Initial release
-- Debian Haskell Group <pkg-haskell-maintainers@lists.alioth.debian.org> Wed, 30 Sep 2015 18:52:46 +0000
+1
View File
@@ -0,0 +1 @@
9
+196
View File
@@ -0,0 +1,196 @@
Source: haskell-postgrest
Maintainer: Debian Haskell Group <pkg-haskell-maintainers@lists.alioth.debian.org>
Priority: extra
Section: haskell
Build-Depends: debhelper (>= 9),
haskell-devscripts (>= 0.8),
cdbs,
ghc,
ghc-prof,
libghc-http-dev,
libghc-http-prof,
libghc-missingh-dev,
libghc-missingh-prof,
libghc-ranged-sets-dev,
libghc-ranged-sets-prof,
libghc-aeson-dev,
libghc-aeson-prof,
libghc-base64-string-dev,
libghc-base64-string-prof,
libghc-bcrypt-dev,
libghc-bcrypt-prof,
libghc-blaze-builder-dev,
libghc-blaze-builder-prof,
libghc-case-insensitive-dev,
libghc-case-insensitive-prof,
libghc-cassava-dev,
libghc-cassava-prof,
libghc-convertible-dev,
libghc-convertible-prof,
libghc-hasql-dev,
libghc-hasql-prof,
libghc-hasql-backend-dev,
libghc-hasql-backend-prof,
libghc-hasql-postgres-dev,
libghc-hasql-postgres-prof,
libghc-http-types-dev,
libghc-http-types-prof,
libghc-jwt-dev,
libghc-jwt-prof,
libghc-mtl-dev,
libghc-mtl-prof,
libghc-network-dev,
libghc-network-prof,
libghc-network-uri-dev,
libghc-network-uri-prof,
libghc-optparse-applicative-dev,
libghc-optparse-applicative-prof,
libghc-regex-base-dev,
libghc-regex-base-prof,
libghc-regex-tdfa-dev,
libghc-regex-tdfa-prof,
libghc-resource-pool-dev,
libghc-resource-pool-prof,
libghc-scientific-dev,
libghc-scientific-prof,
libghc-split-dev,
libghc-split-prof,
libghc-string-conversions-dev,
libghc-string-conversions-prof,
libghc-stringsearch-dev,
libghc-stringsearch-prof,
libghc-text-dev,
libghc-text-prof,
libghc-unordered-containers-dev,
libghc-unordered-containers-prof,
libghc-vector-dev,
libghc-vector-prof,
libghc-wai-dev,
libghc-wai-prof,
libghc-wai-cors-dev,
libghc-wai-cors-prof,
libghc-wai-extra-dev,
libghc-wai-extra-prof,
libghc-wai-middleware-static-dev,
libghc-wai-middleware-static-prof,
libghc-warp-dev,
libghc-warp-prof,
libghc-aeson-dev (>= 0.8),
libghc-bcrypt-dev (>= 0.0.6),
libghc-hasql-dev (>= 0.7.3),
libghc-hasql-dev (<< 0.8),
libghc-hasql-backend-dev (>= 0.4.1),
libghc-hasql-backend-dev (<< 0.5),
libghc-hasql-postgres-dev (>= 0.10.4),
libghc-hasql-postgres-dev (<< 0.11),
libghc-network-dev (>= 2.6),
libghc-network-uri-dev (>= 2.6),
libghc-optparse-applicative-dev (>= 0.11),
libghc-optparse-applicative-dev (<< 0.12),
libghc-wai-dev (>= 3.0.1),
libghc-wai-middleware-static-dev (>= 0.6.0),
libghc-warp-dev (>= 3.0.2),
libghc-quickcheck2-dev,
libghc-heredoc-dev,
libghc-hlint-dev,
libghc-hspec-dev (>= 2.1),
libghc-hspec-dev (<< 2.2),
libghc-hspec-wai-dev,
libghc-hspec-wai-json-dev,
libghc-http-media-dev,
libghc-packdeps-dev,
Build-Depends-Indep: ghc-doc,
libghc-http-doc,
libghc-missingh-doc,
libghc-ranged-sets-doc,
libghc-aeson-doc,
libghc-base64-string-doc,
libghc-bcrypt-doc,
libghc-blaze-builder-doc,
libghc-case-insensitive-doc,
libghc-cassava-doc,
libghc-convertible-doc,
libghc-hasql-doc,
libghc-hasql-backend-doc,
libghc-hasql-postgres-doc,
libghc-http-types-doc,
libghc-jwt-doc,
libghc-mtl-doc,
libghc-network-doc,
libghc-network-uri-doc,
libghc-optparse-applicative-doc,
libghc-regex-base-doc,
libghc-regex-tdfa-doc,
libghc-resource-pool-doc,
libghc-scientific-doc,
libghc-split-doc,
libghc-string-conversions-doc,
libghc-stringsearch-doc,
libghc-text-doc,
libghc-unordered-containers-doc,
libghc-vector-doc,
libghc-wai-doc,
libghc-wai-cors-doc,
libghc-wai-extra-doc,
libghc-wai-middleware-static-doc,
libghc-warp-doc,
Standards-Version: 3.9.6
Homepage: https://github.com/begriffs/postgrest
Description: REST API for any Postgres database
Reads the schema of a PostgreSQL database and creates RESTful routes
for the tables and views, supporting all HTTP verbs that security
permits.
Package: libghc-postgrest-dev
Architecture: any
Depends: ${haskell:Depends},
${misc:Depends},
${shlibs:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Provides: ${haskell:Provides},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
Package: libghc-postgrest-prof
Architecture: any
Depends: ${haskell:Depends},
${misc:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Provides: ${haskell:Provides},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
Package: libghc-postgrest-doc
Architecture: all
Section: doc
Depends: ${haskell:Depends},
${misc:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
Package: haskell-postgrest-utils
Architecture: any
Section: misc
Depends: ${haskell:Depends},
${misc:Depends},
Recommends: ${haskell:Recommends},
Suggests: ${haskell:Suggests},
Conflicts: ${haskell:Conflicts},
Provides: ${haskell:Provides},
Description: ${haskell:ShortDescription}${haskell:ShortBlurb}
${haskell:LongDescription}
.
${haskell:Blurb}
+32
View File
@@ -0,0 +1,32 @@
Format: http://www.debian.org/doc/packaging-manuals/copyright-format/1.0/
Upstream-Name: postgrest
Upstream-Contact: Joe Nelson <joe@begriffs.com>
Source: https://hackage.haskell.org/package/postgrest
Files: *
Copyright: 2014-2015 Joe Nelson <joe@begriffs.com>
License: Expat
Files: debian/*
Copyright: 2015 Fernando Ike <fike@midstorm.org>
License: Expat
License: Expat
Permission is hereby granted, free of charge, to any person obtaining
a copy of this software and associated documentation files (the
"Software"), to deal in the Software without restriction, including
without limitation the rights to use, copy, modify, merge, publish,
distribute, sublicense, and/or sell copies of the Software, and to
permit persons to whom the Software is furnished to do so, subject to
the following conditions:
.
The above copyright notice and this permission notice shall be included
in all copies or substantial portions of the Software.
.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY
CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT,
TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE
SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
+1
View File
@@ -0,0 +1 @@
dist-ghc/build/postgrest/postgrest usr/bin
Vendored Executable
+8
View File
@@ -0,0 +1,8 @@
#!/bin/sh
d=$(dirname $0)
if [ -f /etc/default/postgrest ]; then
. /etc/default/postgrest
fi
POSTGREST_LOG=${POSTGREST_LOG:-/var/log/postgrest/postgrest.log}
exec $d/postgrest "$@" >>$POSTGREST_LOG 2>&1 &
+29
View File
@@ -0,0 +1,29 @@
# run service as
#POSTGREST_USER=postgrest
# log file
#POSTGREST_LOG=/var/log/postgrest/postgrest.log
# database host
#POSTGREST_DBHOST=localhost
# database host
#POSTGREST_DBPORT=5432
# database to use
#POSTGREST_DBNAME=app
# database user
#POSTGREST_DBUSER=authenticator
# database password
#POSTGREST_DBPASS=
# database pool
#POSTGREST_POOL=10
# jwt secret
#POSTGREST_JWT_SECRET=secret
# default schema
#POSTGREST_SCHEMA=public
Vendored Executable
+99
View File
@@ -0,0 +1,99 @@
#!/bin/sh
### BEGIN INIT INFO
# Provides: postgrest
# Required-Start: $local_fs $network postgresql
# Required-Stop: $local_fs $network
# Default-Start: 2 3 4 5
# Default-Stop: 0 1 6
# Description: PostgreSQL REST API daemon
### END INIT INFO
. /lib/lsb/init-functions
if test -f /etc/default/postgrest; then
. /etc/default/postgrest
fi
POSTGREST=/usr/local/bin/postgrest
CONNECTION_STRING="postgres://"
POSTGREST_OPTS=""
POSTGREST_USER=${POSTGREST_USER:-postgrest}
POSTGREST_PORT=${POSTGREST_PORT:-3000}
POSTGREST_DBUSER=${POSTGREST_DBUSER:-authenticator}
#POSTGREST_DBPASS=${POSTGREST_DBPASS:-authenticator}
POSTGREST_DBHOST=${POSTGREST_DBHOST:-localhost}
POSTGREST_DBPORT=${POSTGREST_DBPORT:-5432}
POSTGREST_DBNAME=${POSTGREST_DBNAME:-app}
POSTGREST_DBPOOL=${POSTGREST_DBPOOL:-10}
POSTGREST_ANON=${POSTGREST_ANON:-anonymous}
POSTGREST_JWT_SECRET=${POSTGREST_JWT_SECRET:-secret}
POSTGREST_SCHEMA=${POSTGREST_SCHEMA:-public}
CONNECTION_STRING="$CONNECTION_STRING$POSTGREST_DBUSER"
if [ -n "$POSTGREST_DBPASS" ]; then
CONNECTION_STRING="$CONNECTION_STRING:$POSTGREST_DBPASS"
fi
CONNECTION_STRING="$CONNECTION_STRING@$POSTGREST_DBHOST:$POSTGREST_DBPORT/$POSTGREST_DBNAME"
if [ -n "$POSTGREST_PORT" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --port $POSTGREST_PORT"
fi
if [ -n "$POSTGREST_POOL" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --pool $POSTGREST_POOL"
fi
if [ -n "$POSTGREST_JWT_SECRET" ]; then
#export POSTGREST_JWT_SECRET="$POSTGREST_JWT_SECRET"
POSTGREST_OPTS="$POSTGREST_OPTS --jwt-secret $POSTGREST_JWT_SECRET"
fi
if [ -n "$POSTGREST_SCHEMA" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --schema $POSTGREST_SCHEMA"
fi
if [ -n "$POSTGREST_ANON" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --anonymous $POSTGREST_ANON"
fi
#export CONNECTION_STRING="$CONNECTION_STRING"
START_PARAMS="$CONNECTION_STRING $POSTGREST_OPTS"
start()
{
log_daemon_msg "Starting PostgreSQL REST API daemon" "postgrest" || true
if start-stop-daemon --start --quiet --oknodo --chuid ${POSTGREST_USER} --startas /usr/local/bin/postgrest-wrapper --exec $POSTGREST -- $START_PARAMS; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
}
stop()
{
log_daemon_msg "Stopping PostgreSQL REST API daemon" "postgrest" || true
if start-stop-daemon --stop --quiet --oknodo --exec $POSTGREST; then
log_end_msg 0 || true
else
log_end_msg 1 || true
fi
}
status()
{
status_of_proc $POSTGREST postgrest && exit 0 || exit $?
}
case "$1" in
start)
start
;;
stop)
stop
;;
restart)
stop
start
;;
status)
status
;;
*)
echo "Usage: $0 {start|stop|restart|status}"
esac
Vendored Executable
+10
View File
@@ -0,0 +1,10 @@
#!/usr/bin/make -f
DEB_ENABLE_TESTS = yes
DEB_CABAL_PACKAGE = postgrest
DEB_DEFAULT_COMPILER = ghc
include /usr/share/cdbs/1/rules/debhelper.mk
include /usr/share/cdbs/1/class/hlibrary.mk
build/haskell-postgrest-utils:: build-ghc-stamp
+1
View File
@@ -0,0 +1 @@
3.0 (quilt)
+2
View File
@@ -0,0 +1,2 @@
version=3
http://hackage.haskell.org/package/postgrest/distro-monitor .*-([0-9\.]+)\.(?:zip|tgz|tbz|txz|(?:tar\.(?:gz|bz2|xz)))
+1
View File
@@ -0,0 +1 @@
postgrest.com
+9
View File
@@ -0,0 +1,9 @@
## Deployment
### Heroku
#### Getting Started
#### Using Amazon RDS
### Debian
+9
View File
@@ -0,0 +1,9 @@
## Data Migration
### Sqitch
### Test-Driven Migrations
#### Structural Tests
#### Value Tests with pgTAP
+9
View File
@@ -0,0 +1,9 @@
## Performance
### Benchmarks
### Caching
### Quality of Service
### Tips
+82
View File
@@ -0,0 +1,82 @@
## Security
PostgREST is designed to keep the database at the center of API
security. All authorization happens through database roles and
permissions. It is PostgREST's job to *authenticate* requests --
i.e. verify that a client is who they say they are -- and then let
the database *authorize* client actions.
We use [JSON Web Tokens](http://jwt.io/) to authenticate API requests.
As you'll recall a JWT contains a list of cryptographically signed
claims. PostgREST cares specifically about a claim called `role`.
When request contains a valid JWT with a role claim PostgREST will
switch to the database role with that name for the duration of the
HTTP request. If the client included no (or an invalid) JWT then
PostgREST selects the "anonymous role" which is specified by a
command line arguments to the server on startup.
```js
{
"role": "jdoe123"
}
// Encoded as JWT with a secret of "secret" this becomes
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiamRvZTEyMyJ9.X_ZeWSS9qsKDCDczv8C-GE2fccrPQjOh_ALMZJa5jsU
```
Using JWT allows us to authenticate with external services. A login
service needs merely to share a JWT encryption secret with the
PostgREST server. The secret is also a server command line option.
It is even possible to generate JWT from inside a stored procedure
in your database. Any SQL stored procedure that returns a type whose
name ends in `jwt_claims` will have its return value encoded into
JWT. See the [User Management](http://postgrest.com/examples/users/)
example for details.
### Database Roles
Suppose you start the server like this:
```bash
postgrest postgres://foo@localhost:5432/mydb --anonymous anon
```
This means that `foo` is the so-called *authenticator role* and
`anon` is the anonymous role. When a new HTTP request arrives at the
server the latter is connected to the database as user `foo`. If
no JWT is present, or if it is invalid, or if it does not contain
the role claim then the server changes to the anonymous role with
the query
```sql
SET LOCAL ROLE anon;
```
Otherwise it sets the role to that specified by JWT. For security
your authenticator role should have access to nothing except the
ability to become other users. Supposing you have three roles, one
for anonymous users, one for authors, and another for the authenticator,
you would set it up like this
```sql
CREATE ROLE authenticator NOINHERIT LOGIN;
CREATE ROLE anon;
CREATE ROLE author;
GRANT anon, author TO authenticator;
```
### Row-Level Security
#### Simulated - PostgreSQL <9.5
#### Real - PostgreSQL >=9.5
### Building Auth on top of JWT
#### Basic Auth
#### Github Sign-in
### SSL
+9
View File
@@ -0,0 +1,9 @@
## API Versioning
### Schema Search Path
### Changing a Resource
### Removing a Resource
### Avoiding DB and Client Coupling
+344
View File
@@ -0,0 +1,344 @@
## Requesting Information
### Tables and Views
* ✅ Cacheable, prefetchable
* ✅ Idempotent
The list of accessible tables and views is provided at
```HTTP
GET /
```
Every view and table accessible by the active db role is exposed
in a one-level deep route. For instance the full contents of a table
`people` is returned at
```HTTP
GET /people
```
There are no `deeply/nested/routes`. Each route provides `OPTIONS`,
`GET`, `POST`, `PATCH`, and `DELETE` verbs depending entirely
on database permissions.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Why not provide nested routes? Many APIs allow nesting to
retrieve related information, such as <code>/films/1/director</code>.
We offer a more flexible mechanism (inspired by GraphQL) to embed
related information. It can handle one-to-many and many-to-many
relationships. This is covered in the section about Embedding.</p>
</div>
### Stored Procedures
* ❌ Cannot necessarily be cached or prefetched
* ❌ Not necessarily idempotent
Every stored procedure is accessible under the `/rpc` prefix. The
API endpoint supports only POST which executes the function.
```HTTP
POST /rpc/proc_name
```
PostgREST supports calling procedures with [named
arguments](http://www.postgresql.org/docs/9.4/static/sql-syntax-calling-funcs.html#SQL-SYNTAX-CALLING-FUNCS-NAMED).
Include a JSON object in the request payload and each
key/value of the object will become an argument.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Why the /rpc prefix? One reason is to avoid name collisions
between views and procedures. It also helps emphasize to API
consumers that these functions are not normal restful things.
The functions can have arbitrary and surprising behavior, not
the standard "post creates a resource" thing that users expect
from the other routes.</p>
<p>We considered allowing GET requests for functions that are
marked non-volatile but could not reconcile how to pass in
parameters. Query string arguments are reserved for shaping/filtering
the output, not providing input.</p>
</div>
### Filtering
#### Filtering Rows
You can filter result rows by adding conditions on columns, each
condition a query string parameter. For instance, to return people
aged under 13 years old:
```HTTP
GET /people?age=lt.13
```
Adding multiple parameters conjoins the conditions:
```HTTP
GET /people?age=gte.18&student=is.true
```
These operators are available:
abbreviation | meaning
------------ | -------
eq | equals
gt | greater than
lt | less than
gte | greater than or equal
lte | less than or equal
like | LIKE operator (use * in place of %)
ilike | ILIKE operator (use * in place of %)
@@ | full-text search using to_tsquery
is | checking for exact equality (null,true,false)
in | one of a list of values e.g. `?a=in.1,2,3`
not | negates another operator, see below
To negate any operator, prefix it with `not` like `?a=not.eq.2`.
For more complicated filters (such as those involving condition 1
*OR* condition 2) you will have to create a new view in the database.
Filters may be applied to [computed
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
as well as actual table/view columns, even though the computed
columns will not appear in the output.
#### Filtering Columns
You can customize which columns are returned by using the `select`
parameter:
```HTTP
GET /people?select=age,height,weight
```
To cast the column types, add a double colon
```HTTP
GET /people?select=age::text,height,weight
```
Not all type coercions are possible, and you will get an error
describing any problems from selection or type casting.
The `select` keyword is reserved. You thus cannot filter rows based
on a column named select. Then again it is a reserved SQL keyword
too, hence an unlikely column name.
#### Inside JSONB
PostgreSQL >=9.4.2 supports native JSON columns and can even index
them by internal keys using the `jsonb` column type. PostgREST
allows you to filter results by internal JSON object values. Use
the single- and double-arrows to path into and obtain values, e.g.
```HTTP
GET /stuff?json_col->a->>b=eq.2
```
This query finds rows in `stuff` where `json_col->'a'->>'b'` is
equal to 2 (or "2" -- it coerces as needed). The final arrow must
be the double kind, `->>`, or else PostgREST will not attempt to
look inside the JSON.
### Ordering
The reserved word `order` reorders the response rows. It uses a
comma-separated list of columns and directions:
```HTTP
GET /people?order=age.desc,height.asc
```
If no direction is specified it defaults to ascending order:
```HTTP
GET /people?order=age
```
If you care where nulls are sorted, add `nullsfirst` or `nullslast`:
```HTTP
GET /people?order=age.nullsfirst
GET /people?order=age.desc.nullslast
```
You can also use [computed
columns](http://www.postgresql.org/docs/current/interactive/xfunc-sql.html#XFUNC-SQL-COMPOSITE-FUNCTIONS)
to order the results, even though the computed
columns will not appear in the output.
### Limiting and Pagination
#### Pagination by Limit-Offset
PostgREST uses HTTP range headers for limiting and describing the
size of results. Every response contains the current range and total
results:
```
Range-Unit: items
Content-Range → 0-14/15
```
This means items zero through fourteen are returned out of a total
of fifteen -- i.e. all of them. This information is available in
every response and can help you render pagination controls on the
client. This is a RFC7233-compliant solution that keeps the response
JSON cleaner.
The client can set the limit and offset of a request by setting the
`Range` header. Translate the limit and offset into a range. To
request the first five elements, include these request headers:
```
Range-Unit: items
Range: 0-4
```
You can also use open-ended ranges for an offset with no limit:
`Range: 10-`.
#### Suppressing Counts
Sometimes knowing the total row count of a query is unnecessary and
only adds extra cost to the database query. So you can skip the
count total using a ```Prefer``` header as:
```
Prefer: count=none
```
With count suppressed the PostgREST response will look like:
```
Range-Unit: items
Content-Range → 0-14/*
```
### Embedding Foreign Entities
To help you make fewer requests, PostgREST allows the embedding of
traditional SQL relationships into a response. Suppose you have a
`projects` table which references `clients` through a foreign key
called `client_id`. When listing projects through the API you can
have it embed the client within each project response. For example,
```HTTP
GET /projects?id=eq.1&select=id, name, clients{*}
```
Notice this is the same `select` keyword which is used to choose
which columns to include. When a column name is followed by parentheses
that means to fetch the entire record and nest it. You include a
list of columns inside the parens, or asterisk to request all
columns.
The embedding works for 1-N, N-1, and N-N relationships. That means
you could also ask for a client and all their projects:
```HTTP
GET /clients?id=eq.42&select=id, name, projects{*}
```
In the examples above we asked for all columns in the embedded resource
but the the select query is recursive. You could for instance specify
```HTTP
GET /foo?select=x, y, bar{z, w, baz{*}}
```
You can select not only using table names, but also column names!
To embed the same foreign key row from our client example earlier
you could do the following:
```HTTP
GET /projects?id=eq.1&select=id, name, client_id{*}
```
In the response there will be a `client_id` object containing all
the data for that row.
However, a `client_id` object doesn't make a lot of sense, so you
could do one of two things. Create a view which renames `client_id`
to just `client` (this is the hard way), or just try `client{*}`
in the select parameter! PostgREST supports smart ducktype checking
for common foreign key names, so if your column name ends with
`_id`, `_fk`, or any variation of the two (including camelcase)
you can embed a row with just the name's beginning.
So for a complete example:
```HTTP
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
```HTTP
Accept: text/csv
```
### Singular vs Plural
Many APIs distinguish plural and singular resources, e.g.`/stories`
vs `/stories/1`. Why do we use `/stories?id=eq.1`? It is because a
single resource is for us a row determined by a primary key, and
primary keys can be *compound* (meaning defined across more than
one column). The common urls come from a degenerate case of simple
(and overwhelmingly numeric) primary keys often introduced automatically
be Object Relational Mapping.
For consistency's sake all these endpoints return a JSON array,
`/stories`, `/stories?genre=eq.mystery`, `/stories?id=eq.1`. They
are all filtering a bigger array. However you might want the
last one to return a single JSON object, not an array with one
element. To request a singular response send the header
`Prefer: plurality=singular`.
### Data Schema
As well as issuing a `GET /` to obtain a list of the tables, views,
and stored procedures available, you can get more information about
any particular endpoint.
```HTTP
OPTIONS /my_view
```
This will include the row names, their types, primary key
information, and foreign keys for the given table or view.
<div class="admonition danger">
<p class="admonition-title">Deprecation Warning</p>
<p>Although we currently use the OPTIONS verb for this, some
people <a
href="https://www.mnot.net/blog/2012/10/29/NO_OPTIONS">argue</a> that
this is inappropriate. We are considering a <code>describedby</code>
header link instead.</p>
</div>
### CORS
PostgREST sets highly permissive cross origin resource sharing. It
accepts Ajax requests from any domain.
+210
View File
@@ -0,0 +1,210 @@
## Updating Data
### Record Creation
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
To create a row in a database table post a JSON object whose keys
are the names of the columns you would like to create. Missing keys
will be set to default values when applicable.
```HTTP
POST /table_name
{ "col1": "value1", "col2": "value2" }
```
The response will include a `Location` header describing where to
find the new object. If you would like to get the full object back
in the response to your request, include the header `Prefer:
return=representation`. That way you won't have to make another
HTTP call to discover properties that may have been filled in on
the server side.
### Bulk Insertion
* ❌ Cannot be cached or prefetched
* ❌ Not idempotent
You can POST a JSON array or CSV to insert multiple rows in a single
HTTP request. Note that using CSV requires less parsing on the server
and is **much faster**.
Example of CSV bulk insert. Simply post to a table route with
`Content-Type: text/csv` and include the names of the columns as
the first row. For instance
```HTTP
POST /people
name,age,height
J Doe,62,70
Jonas,10,55
```
An empty field (`,,`) is coerced to an empty string and the reserved
word `NULL` is mapped to the SQL null value. Note that there should
be no spaces between the column names and commas.
Example of JSON bulk insert. Send an array:
```HTTP
POST /people
[
{ "name": "J Doe", "age": 62, "height": 70 },
{ "name": "Janus", "age": 10, "height": 55 }
]
```
If you would like to get the full object back in the response to
your request, include the header `Prefer: return=representation`.
Chances are you only want certain information back, though, like
created ids. You can pass a `select` parameter to affect the shape
of the response (further documented in the [reading](/api/reading/)
page). For instance
```HTTP
POST /people?select=id
[...]
```
returns something like
```json
[ { "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
* ❌ Not idempotent
To change parts of a resource or resources use the `PATCH` verb.
For instance, here is how to mark all young people as children.
```HTTP
PATCH /people?age=lt.13
{
"person_type": "child"
}
```
This affects any rows matched by the url param filters, overwrites
any fields specified in in the payload JSON and leaves the other
fields unaffected. Note that although the payload is not in the
JSON patch format specified by
[RFC6902](https://tools.ietf.org/html/rfc6902), HTTP does not specify
which patch format to use. Our format is more pleasant, meant for
basic field replacements, and not at all "incorrect."
### Deletion
* ❌ Cannot be cached or prefetched
* ✅ Idempotent
Simply use the `DELETE` verb. All records that match your filter
will be removed. For instance deleting inactive users:
```HTTP
DELETE /user?active=is.false
```
### Protecting Dangerous Actions
Notice that it is very easy to delete or update many records at
once. In fact forgetting a filter will affect an entire table!
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>We would like to investigate nginx rules to guard dangerous
actions, perhaps requiring a confirmation header or query param
to perform the action.</p>
<p>You're invited to research this option and contribute to
this documentation.</p>
</div>
+150
View File
@@ -0,0 +1,150 @@
## Multi-Tenant Blog
In our blog app there will be anonymous users and authors. Each
author can create and edit their own posts, and read (but not edit)
the posts of other authors. Anonymous users cannot edit anything
but can sign up for author accounts. Authors can also post comments
on articles.
This example builds off the previous previous [User Management](users/)
one. We had previously created a signup and login system on top of
JWT. We'll use this auth system for the blog. **Run the SQL in the
previous example** first, before continuing with this example.
For your convenience, the complete sql for the blog demo is
[here](https://github.com/begriffs/postgrest/blob/master/schema-templates/blog.sql).
You can try it out in this [vagrant
image](https://github.com/ruslantalpa/blogdemo) as well.
### Adding Blog-Specific Tables
Storing the posts and comments is this simple. The comments do not
form a tree, they are linear under a post.
```sql
create table if not exists
posts (
id bigserial primary key,
title text not null,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
created_at timestamptz not null default current_date
);
create table if not exists
comments (
id bigserial primary key,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
post bigint not null references posts (id)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
```
### Permissions
On top of the `authenticator` and `anon` access granted in the
previous example, blogs have an `author` role with extra permissions.
```sql
create role author;
grant author to authenticator;
grant usage on schema public, basic_auth to author;
-- authors can edit comments/posts
grant select, insert, update, delete
on basic_auth.tokens, basic_auth.users to author;
grant select, insert, update, delete
on table users, posts, comments to author;
grant usage, select on sequence posts_id_seq, comments_id_seq to author;
```
To ensure that authors cannot edit each others' posts and comments
we'll use [row-level
security](http://www.postgresql.org/docs/9.5/static/ddl-rowsecurity.html).
Note that it requires PostgreSQL 9.5 or later.
```sql
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on posts;
create policy authors_eigenedit on posts
using (true)
with check (
author = basic_auth.current_email()
);
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on comments;
create policy authors_eigenedit on comments
using (true)
with check (
author = basic_auth.current_email()
);
```
Finally we need to modify the `users` view from the previous example.
This is because all authors share a single db role. We could have
chosen to assign a new role for every author (all inheriting from
`author`) but we choose to tell them apart by their email addresses.
The addition below prevents authors from seeing each others' info
in the `users` view.
```diff
create or replace view users as
select actual.role as role,
'***'::text as pass,
actual.email as email,
actual.verified as verified
from basic_auth.users as actual,
(select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
) as member_of
where actual.role = member_of.rolname
+ and (
+ actual.role <> 'author'
+ or email = basic_auth.current_email()
+ );
```
### Example client queries
* Top ten most recent posts
```HTTP
GET /posts?order=created_at.desc
Range: 0-9
```
* Single post (randomly chose id=1) with its comments
```HTTP
GET /posts?id=eq.1&select=*,comments{*}
```
* Add a new post
```HTTP
POST /posts
Authorization: Bearer [JWT TOKEN]
{
"title": "My first post",
"body": "Meh, forgot what I wanted to say."
}
```
### Conclusion
Voilà, a blog API. Most of the code ended up being for defining
security. Once you have set up an authentication system, the code
to do application specific things like blog posts and comments is
short. All the front-end routes and verbs are created automatically
for you.
+193
View File
@@ -0,0 +1,193 @@
## External Authentication
API clients authenticate with [JSON Web Tokens](http://jwt.io).
PostgREST does not support any other authentication mechanism
directly, but they can be built on top. In this demo we will build
a system that works with an external authentication server
and integrates with a PostgREST server by sharing the same JWT secret.
For a better understanding of JWT and PostgREST authentication system you should read
the [User Management](users/) example as well.
I'll use a [Rails](http://rubyonrails.org) application using [Devise](https://github.com/plataformatec/devise)
just to make the example more concrete, but this could be replicated for
any other external authentication system using the same principles.
In case Rails is not your cup of tea you can continue reading and
just skip the Ruby code samples. I'll also assume
the use of JQuery for some client-side code samples for the sake of simplicity.
I won't delve into Devise authentication details, for this would require a tutorial on its own,
so I'm assuming that the reader's authentication system is already working.
### Sharing the JWT Secret
Allowing a third party to generate valid JWTs for your PostgREST API
is just a matter of sharing a secret. So you need to give your authenticator
software the same secret that was used in your API server under the ```--jwt-secret```
parameter.
This could be done easly using environment variables. You set a ```JWT_SECRET``` variable
in the environment where you run your rails app and it will be accessible in the global
variable ```ENV['JWT_SECRET']```.
### User Model
We will map each user in this example to two database roles.
So our application users are either ```admin``` or ```customer```.
If they are just visitors (not logged in) to our website they will be ```anonymous```.
One way of mapping users is to add a field in our users table indicating their database role.
I'll add a text field called role to my users table:
```sql
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 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.
Bellow we have the commands to create all roles that will be used:
```sql
CREATE USER authenticator NOINHERIT;
CREATE ROLE anonymous;
CREATE ROLE admin;
CREATE ROLE customer;
GRANT customer, admin, anonymous TO authenticator;
```
### Generating a JWT
Several libraries are available to generate JWT, you will find a very handy list in [their website](http://jwt.io)
under **Libraries**.
To continue our Rails example I'll use the ruby library [json_web_token](https://github.com/garyf/json_web_token).
In order to make the gem available in my Rails project I add the following line to my Gemfile:
```
gem 'json_web_token'
```
Then we create a Rails controller to serve JWTs for my authenticated users.
For this I just open a file ```app/controllers/api_tokens_controller.rb``` with the content:
```ruby
class ApiTokensController < ApplicationController
TOKEN_TTL = 1.hour
def show
unless ENV['JWT_SECRET'].present?
return render json: {error: "you need to have JWT_SECRET configured to get an API token"}, status: 500
end
unless current_user.present?
return render json: {error: "only authenticated users can request the API token"}, status: 401
end
expires_in TOKEN_TTL, public: false
render json: {token: jwt}, status: 200
end
private
def jwt
JsonWebToken.sign(claims, key: ENV['JWT_SECRET'])
end
def claims
# This token will expire 1 hour after being issued
{
role: current_user.role,
user_id: current_user.id.to_s,
exp: (Time.now + TOKEN_TTL).to_i
}
end
end
```
<div class="admonition note">
<p class="admonition-title">Token Time to Live</p>
<p>
In the code above we leverage the HTTP time based cache headers to expire the
endpoint cache at the same time as the token. In this example we have a token
that will be refresh one hour after its issuing time.
That's why both are based on the <code>TOKEN_TTL</code> constant.
</p>
</div>
We also need to create a route in the ```config/routes.rb``` file:
```ruby
resource :api_token, only: [:show]
```
Now, any authenticated user in our rails application can request an api_token making a GET
request to ```/api_token```. This endpoint will return a json object with one property
whose value is the token the API requests should use.
### Orders Endpoint
Here is how to create a view to generate an endpoint ```/orders``` filtered by
the logged in user:
```sql
ALTER DATABASE mydb SET postgrest.claims.user_id TO '';
CREATE OR REPLACE FUNCTION current_user_id()
RETURNS integer
STABLE
LANGUAGE SQL
AS $$
SELECT nullif(current_setting('postgrest.claims.user_id'), '')::integer;
$$;
CREATE SCHEMA private;
CREATE TABLE private.orders (
id serial primary key,
user_id int references users,
created_at timestamp not null default current_timestamp,
updated_at timestamp not null default current_timestamp
);
CREATE VIEW orders AS
SELECT
id, user_id, created_at, updated_at
FROM
private.orders o
WHERE
current_user = 'admin' OR o.user_id = current_user_id();
```
<div class="admonition note">
<p class="admonition-title">DRY priviledge checking conditions</p>
<p>
You can encapsulate conditions that will be commonly used to check for privileges while reading a database row.
We used a function <code>current_user_id()</code> but we could add more conditions to functions
as the system becomes more complex.<br/>
Remeber to mark your functions as <code>STABLE</code> so that PostgreSQL can inline then while planning the query.
</p>
</div>
### Using the JWT
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_token').done(function(data){
$.ajax('/orders', {'Authorization': 'Bearer ' + data.token}).done(function(data){
console.log('Visible Orders: ', data);
})
}).fail(function(){
console.log('Error fetching API token');
})
```
We could also store the token to avoid having to fetch it again in the same page.
### Conclusion
This section explained the implementation details for building an
external authentication system working with PostgREST.
With the previous [User Management](users/) example this should give a clearer
idea of how to set up authentication for your API.
+40
View File
@@ -0,0 +1,40 @@
## Python Client for PostgREST API
### Setup PostgreSQL
This code relies on setting up the PostgreSQL auth functions and grants correctly first. Follow [these instructions](http://postgrest.com/examples/users/).
After completing the PostgreSQL configuration, be sure to create a user with email, password, role, and verified flag. We'll use that user to login in the code below.
### Setup PostgREST
Next, setup PostgREST according to the documentation [http://postgrest.com/install/server/](here).
### Setup Python Client
Finally, we'll install and configure the python client. Follow the instructions in the [README](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/README.md). Be sure to set the [credentials](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/config.in#L3-L5) and [urls](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/config.in#L7-L9) in config.py.
### Python Client Functions
There are four primary functions to the python client:
* login
* construct_jwt_auth
* get_result_size
* get_range
The *login* and *construct_jwt_auth* functions will be required for any REST client using a PostgREST server, since a JWT auth instance is presumed.
The *get_result_size* and *get_range* functions are designed specifically for result sets where pagination is required. You can certainly use them for a single page result set that does not require pagination, but that may be overkill.
### Login
The [login function](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L12-L17) takes email and password strings (credentials.email and credentials.password, respectively from the config.py) and return the response.
### Construct JWT Auth
The [construct_jwt_auth](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L20-L23) function takes the auth response returned by the login function, retrieves the token in the response, and returns a JWT auth instance to the caller. The JWT auth instance can then be used for successive calls to the same PostgREST service.
### Get Result Size
The [get_result_size](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L26-L30) function takes a JWT auth instance calls the URL at urls.data, extracts the size of the result set from the response object and returns the size.
### Get Range
The [get_range](https://github.com/davidthewatson/postgrest_python_requests_client/blob/master/client.py#L26-L30) function takes a beginning range, ending range, page size, and JWT auth instance, gets only that range of the available result set and returns JSON for that result set.
+509
View File
@@ -0,0 +1,509 @@
## Getting Started
### Your First (simple) API
Let's start with the simplest thing possible. We will expose some tables directly for reading and writing by anyone.
Start by making a database
```sh
createdb demo1
```
We'll set it up with a film example (courtesy of [Jonathan Harrington](http://blog.jonharrington.org/postgrest-introduction/)). Copy the following into your clipboard:
```sql
BEGIN;
CREATE TABLE director
(
name text NOT NULL PRIMARY KEY
);
CREATE TABLE film
(
id serial PRIMARY KEY,
title text NOT NULL,
year date NOT NULL,
director text REFERENCES director (name)
ON UPDATE CASCADE ON DELETE CASCADE,
rating real NOT NULL DEFAULT 0,
language text NOT NULL
);
CREATE TABLE festival
(
name text NOT NULL PRIMARY KEY
);
CREATE TABLE competition
(
id serial PRIMARY KEY,
name text NOT NULL,
festival text NOT NULL REFERENCES festival (name)
ON UPDATE CASCADE ON DELETE CASCADE,
year date NOT NULL
);
CREATE TABLE film_nomination
(
id serial PRIMARY KEY,
competition integer NOT NULL REFERENCES competition (id)
ON UPDATE NO ACTION ON DELETE NO ACTION,
film integer NOT NULL REFERENCES film (id)
ON UPDATE CASCADE ON DELETE CASCADE,
won boolean NOT NULL DEFAULT true
);
COMMIT;
```
Apply it to your new database by running
```sh
# On OS X
pbpaste | psql demo1
# Or Linux
# xclip -selection clipboard -o | psql demo1
```
Start the PostgREST server and point it at the new database. (See the [installation instructions](/install/server/).)
```sh
postgrest postgres://postgres:@localhost:5432/demo1 -a postgres --schema public
```
<div class="admonition note">
<p class="admonition-title">Note about database users</p>
<p>If you installed PostgreSQL with Homebrew on Mac then the
database username may be your own login rather than
<code>postgres</code>.</p>
</div>
### Populating Data
Let's use PostgREST to populate the database. Install a REST client such as [Postman](https://chrome.google.com/webstore/detail/postman/fhbjgbiflinjbdggehcddcbncdddomop?hl=en). Now let's insert some data as a bulk post in CSV format:
```HTTP
POST http://localhost:3000/festival
Content-Type: text/csv
name
Venice Film Festival
Cannes Film Festival
```
In Postman it will look like this
![Festival bulk insert in postman](/img/post-festivals.png)
Notice that the post type is `raw` and that `Content-Type: text/csv` set in the Headers tab.
The server returns HTTP 201 Created. Because we inserted more than one item at once there is no `Location` header in the response. However sometimes you want to learn more about items which you just inserted. To have the server include the full results, include the header `Prefer: return=representation`.
At this point if you send a GET request to `/festival` it should return
```json
[
{
"name": "Venice Film Festival"
},
{
"name": "Cannes Film Festival"
}
]
```
Now that you've seen how to do a bulk insert, let's do some more and fully populate the database.
Post the following to `/competition`:
```csv
name,festival,year
Golden Lion,Venice Film Festival,2014-01-01
Palme d'Or,Cannes Film Festival,2014-01-01
```
Now `/director`:
```csv
name
Bertrand Bonello
Atom Egoyan
David Gordon Green
Andrey Konchalovskiy
Mario Martone
Mike Leigh
Roy Andersson
Saverio Costanzo
Alix Delaporte
Jean-Pierre Dardenne
Xiaoshuai Wang
Kaan Müjdeci
Tommy Lee Jones
Nuri Bilge Ceylan
Michel Hazanavicius
Xavier Dolan
Ramin Bahrani
Alice Rohrwacher
Andrew Niccol
Rakhshan Bani-Etemad
David Oelhoffen
Bennett Miller
David Cronenberg
Shin'ya Tsukamoto
Joshua Oppenheimer
Olivier Assayas
Jean-Luc Godard
Alejandro González Iñárritu
Benoît Jacquot
Fatih Akin
Francesco Munzi
Ken Loach
Abel Ferrara
Xavier Beauvois
Naomi Kawase
```
And `/film`:
```csv
title,year,director,rating,language
Chuang ru zhe,2014-01-01,Xiaoshuai Wang,6.19999981,english
The Look of Silence,2014-01-01,Joshua Oppenheimer,8.30000019,Indonesian
Fires on the Plain,2014-01-01,Shin'ya Tsukamoto,5.80000019,Japanese
Far from Men,2014-01-01,David Oelhoffen,7.5,english
Good Kill,2014-01-01,Andrew Niccol,6.0999999,english
Leopardi,2014-01-01,Mario Martone,6.9000001,english
Sivas,2014-01-01,Kaan Müjdeci,7.69999981,english
Black Souls,2014-01-01,Francesco Munzi,7.0999999,english
Three Hearts,2014-01-01,Benoît Jacquot,5.80000019,French
Pasolini,2014-01-01,Abel Ferrara,5.80000019,english
Le dernier coup de marteau,2014-01-01,Alix Delaporte,6.5,english
Manglehorn,2014-01-01,David Gordon Green,7.0999999,english
Hungry Hearts,2014-01-01,Saverio Costanzo,6.4000001,English
Belye nochi pochtalona Alekseya Tryapitsyna,2014-01-01,Andrey Konchalovskiy,6.9000001,Russian
99 Homes,2014-01-01,Ramin Bahrani,7.30000019,english
The Cut,2014-01-01,Fatih Akin,6,Armenian
Birdman: Or (The Unexpected Virtue of Ignorance),2014-01-01,Alejandro González Iñárritu,8,English
La rançon de la gloire,2014-01-01,Xavier Beauvois,5.69999981,French
A Pigeon Sat on a Branch Reflecting on Existence,2014-01-01,Roy Andersson,7.19999981,english
Tales,2014-01-01,Rakhshan Bani-Etemad,6.80000019,english
The Wonders,2014-01-01,Alice Rohrwacher,6.80000019,Italian
Foxcatcher,2014-01-01,Bennett Miller,7.19999981,English
Mr. Turner,2014-01-01,Mike Leigh,7,English
Jimmy's Hall,2014-01-01,Ken Loach,6.69999981,English
The Homesman,2014-01-01,Tommy Lee Jones,6.5999999,English
The Captive,2014-01-01,Atom Egoyan,5.9000001,english
Goodbye to Language,2014-01-01,Jean-Luc Godard,6.19999981,French
The Search,2014-01-01,Michel Hazanavicius,6.9000001,French
Still the Water,2014-01-01,Naomi Kawase,6.9000001,Japanese
Mommy,2014-01-01,Xavier Dolan,8.30000019,French
"Two Days, One Night",2014-01-01,Jean-Pierre Dardenne,7.4000001,French
Maps to the Stars,2014-01-01,David Cronenberg,6.4000001,English
Saint Laurent,2014-01-01,Bertrand Bonello,6.5,French
Clouds of Sils Maria,2014-01-01,Olivier Assayas,6.9000001,english
Winter Sleep,2014-01-01,Nuri Bilge Ceylan,8.5,Turkish
```
Finally `/film_nomination`:
```csv
competition,film,won
1,1,f
1,2,f
1,3,f
1,4,f
1,5,f
1,6,f
1,7,f
1,8,f
1,9,f
1,10,f
1,11,f
1,12,f
1,13,f
1,14,f
1,15,f
1,16,f
1,17,f
1,18,f
1,19,f
1,20,f
2,21,f
2,22,f
2,23,f
2,24,f
2,25,f
2,26,f
2,27,f
2,28,f
2,29,f
2,30,f
2,31,f
2,32,f
2,33,f
2,34,f
2,35,f
```
### Getting and Embedding Data
First let's review which films are stored in the database:
```http
GET http://localhost:3000/film
```
It gives us back a list of JSON objects. What if we care only about the film titles? Use `select` to shape the output:
```http
GET http://localhost:3000/film?select=title
```
```json
[
{
"title": "Chuang ru zhe"
},
{
"title": "The Look of Silence"
},
{
"title": "Fires on the Plain"
},
...
]
```
Here is where it gets cool. PostgREST can embed objects in its response through foreign key relationships. Earlier we created a join table called `film_nomination`. It joins films and competitions. We can ask the server about the structure of this table:
```
OPTIONS http://localhost:3000/film_nomination
```
```json
{
"pkey": [
"id"
],
"columns": [
{
"references": null,
"default": "nextval('film_nomination_id_seq'::regclass)",
"precision": 32,
"updatable": true,
"schema": "public",
"name": "id",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 1
},
{
"references": {
"schema": "public",
"column": "id",
"table": "competition"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "public",
"name": "competition",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 2
},
{
"references": {
"schema": "public",
"column": "id",
"table": "film"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "public",
"name": "film",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 3
},
{
"references": null,
"default": "true",
"precision": null,
"updatable": true,
"schema": "public",
"name": "won",
"type": "boolean",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 4
}
]
}
```
From this you can see that the columns `film` and `competition` reference their eponymous tables. Let's ask the server for each film along with names of the competitions it entered. You don't have to do any custom coding. Send this query:
```http
GET http://localhost:3000/film?select=title,competition{name}
```
```json
[
{
"title": "Chuang ru zhe",
"competition": [
{
"name": "Golden Lion"
}
]
},
{
"title": "The Look of Silence",
"competition": [
{
"name": "Golden Lion"
}
]
},
...
]
```
The relation flows both ways. Here is how to get the name of each competition's name and the movies shown at it.
```http
GET http://localhost:3000/competition?select=name,film{title}
```
```json
[
{
"name": "Golden Lion",
"film": [
{
"title": "Chuang ru zhe"
},
{
"title": "The Look of Silence"
},
...
]
},
{
"name": "Palme d'Or",
"film": [
{
"title": "The Wonders"
},
{
"title": "Foxcatcher"
},
...
]
}
]
```
Why not learn about the directors too? There is a many-to-one relation directly between films and directors. We can alter our previous query to include directors in its results.
```http
GET http://localhost:3000/competition?select=name,film{title,director{*}}
```
```json
[
{
"name": "Golden Lion",
"film": [
{
"title": "Manglehorn",
"director": {
"name": "David Gordon Green"
}
},
{
"title": "Belye nochi pochtalona Alekseya Tryapitsyna",
"director": {
"name": "Andrey Konchalovskiy"
}
},
...
]
},
...
]
```
### Singular Responses
How do we ask for a single film, for instance the second one we inserted?
```http
GET http://localhost:3000/film?id=eq.2
```
It returns
```json
[
{
"id": 2,
"title": "The Look of Silence",
"year": "2014-01-01",
"director": "Joshua Oppenheimer",
"rating": 8.3,
"language": "Indonesian"
}
]
```
Like any query, it gives us a result *set*, in this case an array with one element. However you and I know that `id` is a primary key, it will never return more than one result. We might want it returned as a JSON object, not an array. To express this preference include the header `Prefer: plurality=singular`. It will respond with
```json
{
"id": 2,
"title": "The Look of Silence",
"year": "2014-01-01",
"director": "Joshua Oppenheimer",
"rating": 8.3,
"language": "Indonesian"
}
```
<div class="admonition note">
<p class="admonition-title">Why this approach to singular responses?</p>
<p>
PostgREST knows which columns comprise a primary key for a
table, so why not automatically choose plurality=singular when
these column filters are present? The fact is it could come as a
shock to a client that by adding one more filter condition it can
change the entire response format.
</p>
<p>
Then why not expose another kind of route such as /film/2 to indicate
one particular film? Because this does not accommodate compound keys.
The convention complects a plurality preference with table key
assumptions. We should separate concerns.
</p>
<p>
It turns out you can still have routes like /film/2. Use a
proxy such as Nginx. It can rewrite routes such as /films/2
into /films?id=eq.2 and add the Prefer header to make the results
singular.
</p>
</div>
### Conclusion
This tutorial showed how to create a database with a basic schema, run PostgREST, and interact with the API. The next tutorial will show how to enable security for a multi-tenant blogging API.
+504
View File
@@ -0,0 +1,504 @@
## User Management
API clients authenticate with [JSON Web Tokens](http://jwt.io).
PostgREST does not support any other authentication mechanism
directly, but they can be built on top. In this demo we will build
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 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 specifically about a claim called `role`. When a client includes
a `role` claim PostgREST executes their request using that database
role.
How would a client include a role claim, or claims in general?
Without knowing the server JWT secret a client cannot create a
claim. The only place to get a JWT is from the PostgREST server or
from another service sharing the secret and acting on its behalf.
We'll use a stored procedure returning type `jwt_claims` which is
a special type causing the server to encrypt and sign the return
value.
### Storing Users and Passwords
We create a database schema especially for auth information. We'll
also need the postgres extension
[pgcrypto](http://www.postgresql.org/docs/current/static/pgcrypto.html).
```sql
create extension if not exists pgcrypto;
-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
-- refer to helpers and tables inside.
create schema if not exists basic_auth;
```
Next a table to store the mapping from usernames and passwords to
database roles. The code below includes triggers and functions to
encrypt the password and ensure the role exists.
```sql
create table if not exists
basic_auth.users (
email text primary key check ( email ~* '^.+@.+\..+$' ),
pass text not null check (length(pass) < 512),
role name not null check (length(role) < 512),
verified boolean not null default false
-- If you like add more columns, or a json column
);
create or replace function
basic_auth.check_role_exists() returns trigger
language plpgsql
as $$
begin
if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
raise foreign_key_violation using message =
'unknown database role: ' || new.role;
return null;
end if;
return new;
end
$$;
drop trigger if exists ensure_user_role_exists on basic_auth.users;
create constraint trigger ensure_user_role_exists
after insert or update on basic_auth.users
for each row
execute procedure basic_auth.check_role_exists();
create or replace function
basic_auth.encrypt_pass() returns trigger
language plpgsql
as $$
begin
if tg_op = 'INSERT' or new.pass <> old.pass then
new.pass = crypt(new.pass, gen_salt('bf'));
end if;
return new;
end
$$;
drop trigger if exists encrypt_pass on basic_auth.users;
create trigger encrypt_pass
before insert or update on basic_auth.users
for each row
execute procedure basic_auth.encrypt_pass();
```
With the table in place we can make a helper to check passwords.
It returns the database role for a user if the email and password
are correct.
```sql
create or replace function
basic_auth.user_role(email text, pass text) returns name
language plpgsql
as $$
begin
return (
select role from basic_auth.users
where users.email = user_role.email
and users.pass = crypt(user_role.pass, users.pass)
);
end;
$$;
```
### Password Reset
When a user requests a password reset or signs up we create a token
they will use later to prove their identity. The tokens go in this
table.
```sql
drop type if exists token_type_enum cascade;
create type token_type_enum as enum ('validation', 'reset');
create table if not exists
basic_auth.tokens (
token uuid primary key,
token_type token_type_enum not null,
email text not null references basic_auth.users (email)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
```
In the main schema (as opposed to the `basic_auth` schema) we expose
a password reset request function. HTTP clients will call it. The
function takes the email address of the user.
```sql
create or replace function
request_password_reset(email text) returns void
language plpgsql
as $$
declare
tok uuid;
begin
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = request_password_reset.email;
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',
json_build_object(
'email', request_password_reset.email,
'token', tok,
'token_type', 'reset'
)::text
);
end;
$$;
```
This function does not send any emails. It sends a postgres
[NOTIFY](http://www.postgresql.org/docs/current/static/sql-notify.html)
command. External programs such as a mailer listen for this event
and do the work. The most robust way to process these signals is
by pushing them onto work queues. Here are two programs to do that:
1. [aweber/pgsql-listen-exchange](https://github.com/aweber/pgsql-listen-exchange) for RabbitMQ
2. [SpiderOak/skeeter](https://github.com/SpiderOak/skeeter) for ZeroMQ
For experimentation you don't need that though. Here's a sample
Node program that listens for the events and logs them to stdout.
```js
var PS = require('pg-pubsub');
if(process.argv.length !== 3) {
console.log("USAGE: DB_URL");
process.exit(2);
}
var url = process.argv[2],
ps = new PS(url);
// password reset request events
ps.addChannel('reset', console.log);
// email validation required event
ps.addChannel('validate', console.log);
// modify me to send emails
```
Once the user has a reset token they can use it as an argument to
the password reset function, calling it through the PostgREST RPC
interface.
```sql
create or replace function
reset_password(email text, token uuid, pass text)
returns void
language plpgsql
as $$
declare
tok uuid;
begin
if exists(select 1 from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset') then
update basic_auth.users set pass=reset_password.pass
where users.email = reset_password.email;
delete from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset';
else
raise invalid_password using message =
'invalid user or token';
end if;
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = reset_password.email;
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',
json_build_object(
'email', reset_password.email,
'token', tok
)::text
);
end;
$$;
```
### Email Validation
This is similar to password resets. Once again we generate a token.
It differs in that there is a trigger to send validations when a
new login is added to the users table.
```sql
create or replace function
basic_auth.send_validation() returns trigger
language plpgsql
as $$
declare
tok uuid;
begin
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'validation', new.email);
perform pg_notify('validate',
json_build_object(
'email', new.email,
'token', tok,
'token_type', 'validation'
)::text
);
return new;
end
$$;
drop trigger if exists send_validation on basic_auth.users;
create trigger send_validation
after insert on basic_auth.users
for each row
execute procedure basic_auth.send_validation();
```
### Editing Own User
We'll construct a redacted view for users. It hides passwords and
shows only those users whose roles the currently logged in user has
db permission to access.
```sql
create or replace view users as
select actual.role as role,
'***'::text as pass,
actual.email as email,
actual.verified as verified
from basic_auth.users as actual,
(select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
) as member_of
where actual.role = member_of.rolname;
-- can also add restriction that current_setting('postgrest.claims.email')
-- is equal to email so that user can only see themselves
```
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
of privileges. Someone could update their own row and change their
role to become more powerful.
We'll handle updates with a trigger, but we'll need a helper function
to prevent an escalation of privileges.
```sql
create or replace function
basic_auth.clearance_for_role(u name) returns void as
$$
declare
ok boolean;
begin
select exists (
select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
and rolname = u
) into ok;
if not ok then
raise invalid_password using message =
'current user not member of role ' || u;
end if;
end
$$ LANGUAGE plpgsql;
```
With the above function we can now make a safe trigger to allow
user updates.
```sql
create or replace function
update_users() returns trigger
language plpgsql
AS $$
begin
if tg_op = 'INSERT' then
perform basic_auth.clearance_for_role(new.role);
insert into basic_auth.users
(role, pass, email, verified)
values
(new.role, new.pass, new.email,
coalesce(new.verified, false));
return new;
elsif tg_op = 'UPDATE' then
-- no need to check clearance for old.role because
-- an ineligible row would not have been available to update (http 404)
perform basic_auth.clearance_for_role(new.role);
update basic_auth.users set
email = new.email,
role = new.role,
pass = new.pass,
verified = coalesce(new.verified, old.verified, false)
where email = old.email;
return new;
elsif tg_op = 'DELETE' then
-- no need to check clearance for old.role (see previous case)
delete from basic_auth.users
where basic_auth.email = old.email;
return null;
end if;
end
$$;
drop trigger if exists update_users on users;
create trigger update_users
instead of insert or update or delete on
users for each row execute procedure update_users();
```
Finally add a public function people can use to sign up. You can
hard code a default db role in it. It alters the underlying
`basic_auth.users` so you can set whatever role you want without
restriction.
```sql
create or replace function
signup(email text, pass text) returns void
as $$
insert into basic_auth.users (email, pass, role) values
(signup.email, signup.pass, 'hardcoded-role-here');
$$ language sql;
```
### Generating JWT
As mentioned at the start, clients authenticate with JWT. PostgREST
has a special convention to allow your sql functions to return JWT.
Any function that returns a type whose name ends in `jwt_claims` will
have its return value encoded. For instance, let's make a login function
which consults our users table.
First create a return type:
```sql
drop type if exists basic_auth.jwt_claims cascade;
create type basic_auth.jwt_claims AS (role text, email text);
```
And now the function:
```sql
create or replace function
login(email text, pass text) returns basic_auth.jwt_claims
language plpgsql
as $$
declare
_role name;
result basic_auth.jwt_claims;
begin
select basic_auth.user_role(email, pass) into _role;
if _role is null then
raise invalid_password using message = 'invalid user or password';
end if;
-- TODO; check verified flag if you care whether users
-- have validated their emails
select _role as role, login.email as email into result;
return result;
end;
$$;
```
An API request to login would look like this.
```HTTP
POST /rpc/login
{ "email": "foo@bar.com", "pass": "foobar" }
```
Response
```json
{
"token": "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.KHwYdK9dAMAg-MGCQXuDiFuvbmW-y8FjfYIcMrETnto"
}
```
Try decoding the token at [jwt.io](http://jwt.io/). (It was encoded
with a secret of `secret` which is the default.) To use this token
in a future API request include it in an `Authorization` request
header.
```HTTP
Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJlbWFpbCI6ImZvb0BiYXIuY29tIiwicm9sZSI6ImF1dGhvciJ9.KHwYdK9dAMAg-MGCQXuDiFuvbmW-y8FjfYIcMrETnto
```
### Same-Role Users
You may not want a separate db role for every user. You can distinguish
one user from another in SQL by examining the JWT claims which
PostgREST makes available in the SQL variable `postgrest.claims`.
Here's a function to get the email of the currently authenticated
user.
```sql
create or replace function
basic_auth.current_email() returns text
language plpgsql
as $$
begin
return current_setting('postgrest.claims.email');
exception
-- handle unrecognized configuration parameter error
when undefined_object then return '';
end;
$$;
```
Remember that the `login` function set the claims `email` and `role`.
You can modify `login` to set other claims as well if they are
useful for your other SQL functions to reference later.
### Permissions
Basic table-level permissions. We'll add an the `authenticator`
role which can't do anything itself other than switch into other
roles as directed by JWT.
```sql
create role anon;
create role authenticator noinherit;
grant anon to authenticator;
grant usage on schema public, basic_auth to anon;
-- anon can create new logins
grant insert on table basic_auth.users, basic_auth.tokens to anon;
grant select on table pg_authid, basic_auth.users to anon;
grant execute on function
login(text,text),
request_password_reset(text),
reset_password(text,uuid,text),
signup(text, text)
to anon;
```
### Conclusion
This section explained the implementation details for building a
password based authentication system in pure sql. The next example
will put it to work in a multi-tenant blogging API.
BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 KiB

BIN
View File
Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 54 KiB

+91
View File
@@ -0,0 +1,91 @@
<style>
.videoWrapper {
position: relative;
padding-bottom: 56.25%; /* 16:9 */
padding-top: 25px;
height: 0;
}
.videoWrapper iframe {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
}
</style>
![PostgREST logo](img/logo.png)
## Introduction
PostgREST is a standalone web server that turns your database directly into a RESTful API. The structural constraints and permissions in the database determine the API endpoints and operations.
This guide explains how to install the software and provides practical examples of its use. You'll learn how to build a fast, versioned, secure API and how to deploy it to production.
The project has a friendly and growing community. Here are some ways to get help or get involved:
* The project [chat room](https://gitter.im/begriffs/postgrest)
* Report or search [issues](https://github.com/begriffs/postgrest/issues)
### Motivation
Using PostgREST is an alternative to manual CRUD programming. Custom API servers suffer problems. Writing business logic often duplicates, ignores or hobbles database structure. Object-relational mapping is a leaky abstraction leading to slow imperative code. The PostgREST philosophy establishes a single declarative source of truth: the data itself.
#### Declarative Programming
It's easier to ask Postgres to join data for you and let its query planner figure out the details than to loop through rows yourself. It's easier to assign permissions to db objects than to add guards in controllers. (This is especially true for cascading permissions in data dependencies.) It's easier set constraints than to litter code with sanity checks.
#### Leakproof Abstraction
There is no ORM involved. Creating new views happens in SQL with known performance implications. A database administrator can now create an API from scratch with no custom programming.
#### Embracing the Relational Model
In 1970 E. F. Codd criticized the then-dominant hierarchical model of databases in his article <a href="https://www.seas.upenn.edu/~zives/03f/cis550/codd.pdf">A Relational Model of Data for Large Shared Data Banks</a>. Reading the article reveals a striking similarity between hierarchical databases and nested http routes. With PostgREST we attempt to use flexible filtering and embedding rather than nested routes.
#### One Thing Well
PostgREST has a focused scope. It works well with other tools like Nginx. This forces you to cleanly separate the data-centric CRUD operations from other concerns. Use a collection of sharp tools rather than building a big ball of mud.
#### Shared Improvements
As with any open source project, we all gain from features and fixes in the tool. It's more beneficial than improvements locked inextricably within custom codebases.
### Intro Video
Some things have changed since this video was created but the basics are the same. Learn the big vision behind automating APIs.
<div class="videoWrapper">
<iframe src="https://player.vimeo.com/video/115668217" frameborder="0" webkitallowfullscreen mozallowfullscreen allowfullscreen></iframe>
</div>
### Myths
#### You have to make tons of stored procs and triggers
Modern PostgreSQL features like auto-updatable views and computed columns make this mostly unnecessary. Triggers do play a part, but generally not for irksome boilerplate. When they are required triggers are preferable to ad-hoc app code anyway, since the former work reliably for any codepath.
#### Exposing the database destroys encapsulation
PostgREST does versioning through database schemas. This allows you to expose tables and views without making the app brittle. Underlying tables can be superseded and hidden behind public facing views. The chapter about versioning shows how to do this.
### Conventions
This guide contains highlighted notes and tangential information interspersed with the text.
<div class="admonition note">
<p class="admonition-title">Design Consideration</p>
<p>Contains history which informed the current design. Sometimes it discusses unavoidable tradeoffs or a point of theory.</p>
</div>
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>Points out things we know we want to add or improve. They might give you ideas for ways to contribute to the project.</p>
</div>
<div class="admonition danger">
<p class="admonition-title">Deprecation Warning</p>
<p>Alerts you to features which will be removed in the next major (breaking) release.</p>
</div>
+26
View File
@@ -0,0 +1,26 @@
## Ecosystem
### Client-Side Libraries
* [calebmer/postgrest-client](https://github.com/calebmer/postgrest-client) - Advanced JS client for the PostgREST API
* [mithril.postgrest](https://github.com/catarse/mithril.postgrest) - Mithril plugin to create and authenticate requests
* [lewisjared/postgrest-request](https://github.com/lewisjared/postgrest-request) - node interface to postgrest instances
* [JarvusInnovations/jarvus-postgrest-apikit](https://github.com/JarvusInnovations/jarvus-postgrest-apikit) - Sencha framework package for binding models/stores/proxies to PostgREST tables
* [davidthewatson/postgrest_python_requests_client](https://github.com/davidthewatson/postgrest_python_requests_client) - python client featuring JWT auth and pagination of result sets
### Extensions
* [srid/spas](https://github.com/srid/spas) - allow file uploads and basic auth
### Example Apps
* [ruslantalpa/blogdemo](https://github.com/ruslantalpa/blogdemo) - blog api demo in a vagrant image
* [timwis/ext-postgrest-crud](https://github.com/timwis/ext-postgrest-crud) - browser-based spreadsheet
* [srid/chronicle](https://github.com/srid/chronicle#deploying-to-heroku) - tracking a tree of personal memories
* [begriffs/postgrest-example](https://github.com/begriffs/postgrest-example) - how to configure a db for use as an API
* [marmelab/ng-admin-postgrest](https://github.com/marmelab/ng-admin-postgrest) - automatic database admin panel
* [tyrchen/goodfilm](https://github.com/tyrchen/goodfilm) - example film api
### In Production
* [Catarse](https://www.catarse.me/)
+181
View File
@@ -0,0 +1,181 @@
## Installation
### Installing from Pre-Built Release
The [release page](https://github.com/begriffs/postgrest/releases/latest)
has precompiled binaries for Mac OS X, Windows, and several Linux
distros. Extract the tarball and run the binary inside with no
arguments to see usage instructions:
```sh
# Untar the release (available at https://github.com/begriffs/postgrest/releases/latest)
$ tar zxf postgrest-[version]-[platform].tar.xz
# Try running it
$ ./postgrest
# You should see a usage help message
```
<div class="admonition warning">
<p class="admonition-title">Invitation to Contribute</p>
<p>I currently build the binaries manually for each architecture.
It would be nice to set up an automated build matrix for various
architectures. It should support Mac, Windows and 32- and 64-bit
versions of
<ul><li>Scientific Linux 6</li><li>CentOS</li><li>RHEL 6</li></ul></p>
</div>
### Building from Source
When a prebuilt binary does not exist for your system you can build
the project from source. You'll also need to do this if you want
to help with development.
[Stack](https://github.com/commercialhaskell/stack) makes it easy.
It will install any necessary Haskell dependencies on your system.
* [Install Stack](http://docs.haskellstack.org/en/stable/README.html#how-to-install) for your platform
```bash
#ubuntu example
#See the link above for other operating systems
wget -q -O- https://s3.amazonaws.com/download.fpcomplete.com/ubuntu/fpco.key | sudo apt-key add -
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
stack build --install-ghc
sudo stack install --allow-different-user --local-bin-path /usr/local/bin
```
* Run the server
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 -a anon_user [other flags]
```
The user in the connection string is the "authenticator role," i.e.
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 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 (required)</dt>
<dd>The database role used to execute commands for those requests
which provide no JWT authorization.</dd>
<dt>-s, --schema</dt>
<dd>The db schema which you want to expose as an API. For historical
reasons it defaults to <code>1</code>, but you're more likely
to want to choose a value of <code>public</code>.</dd>
<dt>-j, --jwt-secret</dt>
<dd>The secret passphrase used to encrypt JWT tokens. Defaults to
<code>secret</code> but do not use the default in production!
Load-balanced PostgREST servers should share the same secret.</dd>
<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>
<dt>-m, --max-rows</dt>
<dd>Max number of rows to return in a read request. The default is
no limit.</dd>
</dl>
<div class="admonition note">
<p class="admonition-title">Hiding Password from Process List</p>
<p>Passing the database password and JWT secret as naked
parameters might not be a good idea because the parameters are
visible in a <code>ps</code> listing. One solution is to set
environment variables such as PASS and use <code>$PASS</code>
in the connection string. Another is to use a user-specific
<a
href="http://www.postgresql.org/docs/current/static/libpq-pgpass.html">.pgpass</a>
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
```bash
# Ensure brew is up to date
brew update
# Check for any problems with brew's setup
brew doctor
# Install the postgrest package
brew install postgrest
```
This will automatically install PostgreSQL as a dependency (see the [Installing PostgreSQL](#installing-postgresql) section for setup instructions). The process tends to take up to 15 minutes to install the package and its dependencies.
After installation completes, the tool is added to your $PATH and can be used from anywhere with:
```bash
postgrest --help
```
### Installing PostgreSQL
To use PostgREST you will need an underlying database (PostgreSQL version 9.3 or greater is required). You can use something like Amazon [RDS](https://aws.amazon.com/rds/) but installing your own locally is cheaper and more convenient for development.
* [Instructions for OS X](http://exponential.io/blog/2015/02/21/install-postgresql-on-mac-os-x-via-brew/)
* [Instructions for Ubuntu 14.04](https://www.digitalocean.com/community/tutorials/how-to-install-and-use-postgresql-on-ubuntu-14-04)
* [Installer for Windows](http://www.enterprisedb.com/products-services-training/pgdownload#windows)
+28
View File
@@ -0,0 +1,28 @@
site_name: PostgREST
site_url: http://postgrest.com
site_description: Building declarative APIs
site_author: Joe Nelson
site_favicon: favicon.ico
repo_url: https://github.com/begriffs/postgrest
pages:
- Home: index.md
- Install:
- The Server: install/server.md
- Ecosystem: install/ecosystem.md
- API:
- Reading: api/reading.md
- Writing: api/writing.md
- Admin:
- Security: admin/security.md
- Versioning: admin/versioning.md
- Migration: admin/migration.md
- Deployment: admin/deployment.md
- Performance: admin/performance.md
- Examples:
- Getting Started: examples/start.md
- User Management: examples/users.md
- Multi-Tenant Blog: examples/blog.md
- External Authentication: examples/external_auth.md
- Python Client: examples/python-requests-jwt.md
+177 -70
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.2.6.0
version: 0.3.1.1
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
@@ -12,90 +12,197 @@ maintainer: cred+github@begriffs.com
category: Web
build-type: Simple
cabal-version: >=1.10
source-repository head
type: git
location: git://github.com/begriffs/postgrest.git
Flag CI
Description: No warnings allowed in continuous integration
Manual: True
Default: False
executable postgrest
main-is: Main.hs
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 && < 0.10) || (>= 0.11 && < 0.12)
, base >= 4.8 && < 5
, bytestring
, case-insensitive
, cassava
, containers
, contravariant
, errors
, 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
, regex-tdfa
, safe >= 0.3 && < 0.4
, scientific
, string-conversions
, text
, time
, unordered-containers
, vector
, wai >= 3.0.1
, wai-cors
, wai-extra
, wai-middleware-static >= 0.6.0
, warp >= 3.1.0
, HTTP
, Ranged-sets
if !os(windows)
build-depends: unix >= 2.7 && < 3
hs-source-dirs: src
other-modules: Paths_postgrest
, PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.Parsers
, PostgREST.DbStructure
, PostgREST.QueryBuilder
, PostgREST.RangeQuery
, PostgREST.ApiRequest
, PostgREST.Types
library
default-language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
build-depends: base >=4.6 && <5
, hasql == 0.7.*, hasql-backend
, hasql-postgres == 0.10.*
, warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors
, wai-middleware-static >= 0.6.0
, HTTP, convertible, http-types
build-depends: aeson
, base >=4.6 && <5
, bytestring
, case-insensitive
, scientific, time
, aeson, network >= 2.6
, bytestring, text, split, string-conversions
, stringsearch
, containers, unordered-containers
, optparse-applicative == 0.11.*
, regex-base, regex-tdfa
, regex-tdfa-text
, Ranged-sets
, transformers, MissingH
, bcrypt >= 0.0.6, base64-string
, network-uri >= 2.6
, resource-pool
, blaze-builder
, vector
, cassava
, containers
, contravariant
, errors
, hasql
, hasql-transaction
, hasql-pool
, http-types
, interpolatedstring-perl6
, jwt
, lens
, lens-aeson
, mtl
Other-Modules: App
, Auth
, Config
, Error
, Middleware
, PgQuery
, PgStructure
, RangeQuery
, Types
, optparse-applicative
, parsec
, regex-tdfa
, safe
, scientific
, string-conversions
, text
, time
, unordered-containers
, vector
, HTTP
, Ranged-sets
, wai >= 3.0.1
, wai-cors
, wai-extra
, wai-middleware-static >= 0.6.0
, warp >= 3.1.0
Other-Modules: Paths_postgrest
Exposed-Modules: PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.Parsers
, PostgREST.DbStructure
, PostgREST.QueryBuilder
, PostgREST.RangeQuery
, PostgREST.ApiRequest
, PostgREST.Types
hs-source-dirs: src
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
ghc-options: -Wall -W -Werror
Main-Is: Main.hs
Other-Modules: App
, Auth
, Config
, Error
, Middleware
, PgQuery
, PgStructure
, RangeQuery
, Types
, Spec
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
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.Parsers
, PostgREST.DbStructure
, PostgREST.QueryBuilder
, PostgREST.RangeQuery
, PostgREST.ApiRequest
, PostgREST.Types
, SpecHelper
Build-Depends: base, hspec >= 2.1.2, QuickCheck
, hspec-wai >= 0.5.0, hspec-wai-json
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, packdeps, hlint
, HTTP, convertible
, TestTypes
Build-Depends: aeson
, async
, base
, base64-string
, bytestring
, case-insensitive
, wai-extra, wai-cors, containers
, wai-middleware-static
, http-types, scientific, time
, bytestring, aeson, network
, text, optparse-applicative
, stringsearch
, unordered-containers
, regex-base
, string-conversions
, http-media, regex-tdfa
, regex-tdfa-text
, Ranged-sets
, transformers, MissingH, split
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
, cassava
, containers
, contravariant
, errors
, hasql
, hasql-pool
, hasql-transaction
, heredoc
, hspec == 2.2.*
, hspec-wai
, hspec-wai-json
, http-types
, interpolatedstring-perl6
, jwt
, lens
, lens-aeson
, monad-control
, mtl
, optparse-applicative
, parsec
, process
, regex-tdfa
, safe
, scientific
, string-conversions
, text
, time
, transformers
, transformers-base
, unordered-containers
, unix
, vector
, wai
, wai-cors
, wai-extra
, wai-middleware-static
, warp
, HTTP
, Ranged-sets
+380
View File
@@ -0,0 +1,380 @@
-------------------------------------------------------------------------------
-- Adapted from https://github.com/robconery/pg-auth
begin;
-- comment out the role creation statements if
-- you want to run this script more than once
create role anon;
create role author;
create role authenticator noinherit;
grant anon, author to authenticator;
create extension if not exists pgcrypto;
-- We put things inside the basic_auth schema to hide
-- them from public view. Certain public procs/views will
-- refer to helpers and tables inside.
create schema if not exists basic_auth;
-------------------------------------------------------------------------------
-- Utility functions
create or replace function
basic_auth.clearance_for_role(u name) returns void as
$$
declare
ok boolean;
begin
select exists (
select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
and rolname = u
) into ok;
if not ok then
raise invalid_password using message =
'current user not member of role ' || u;
end if;
end
$$ LANGUAGE plpgsql;
-------------------------------------------------------------------------------
-- Users storage and constraints
create table if not exists
basic_auth.users (
email text primary key check ( email ~* '^.+@.+\..+$' ),
pass text not null check (length(pass) < 512),
role name not null check (length(role) < 512),
verified boolean not null default false
-- If you like add more columns, or a json column
);
create or replace function
basic_auth.check_role_exists() returns trigger
language plpgsql
as $$
begin
if not exists (select 1 from pg_roles as r where r.rolname = new.role) then
raise foreign_key_violation using message =
'unknown database role: ' || new.role;
return null;
end if;
return new;
end
$$;
drop trigger if exists ensure_user_role_exists on basic_auth.users;
create constraint trigger ensure_user_role_exists
after insert or update on basic_auth.users
for each row
execute procedure basic_auth.check_role_exists();
create or replace function
basic_auth.encrypt_pass() returns trigger
language plpgsql
as $$
begin
if tg_op = 'INSERT' or new.pass <> old.pass then
new.pass = crypt(new.pass, gen_salt('bf'));
end if;
return new;
end
$$;
drop trigger if exists encrypt_pass on basic_auth.users;
create trigger encrypt_pass
before insert or update on basic_auth.users
for each row
execute procedure basic_auth.encrypt_pass();
create or replace function
basic_auth.send_validation() returns trigger
language plpgsql
as $$
declare
tok uuid;
begin
select gen_random_uuid() into tok;
insert into basic_auth.tokens (token, token_type, email)
values (tok, 'validation', new.email);
perform pg_notify('validate',
json_build_object(
'email', new.email,
'token', tok,
'token_type', 'validation'
)::text
);
return new;
end
$$;
drop trigger if exists send_validation on basic_auth.users;
create trigger send_validation
after insert on basic_auth.users
for each row
execute procedure basic_auth.send_validation();
-------------------------------------------------------------------------------
-- Email Validation and Password Reset
drop type if exists token_type_enum cascade;
create type token_type_enum as enum ('validation', 'reset');
create table if not exists
basic_auth.tokens (
token uuid primary key,
token_type token_type_enum not null,
email text not null references basic_auth.users (email)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
-------------------------------------------------------------------------------
-- Login helper
create or replace function
basic_auth.user_role(email text, pass text) returns name
language plpgsql
as $$
begin
return (
select role from basic_auth.users
where users.email = user_role.email
and users.pass = crypt(user_role.pass, users.pass)
);
end;
$$;
create or replace function
basic_auth.current_email() returns text
language plpgsql
as $$
begin
return current_setting('postgrest.claims.email');
exception
-- handle unrecognized configuration parameter error
when undefined_object then return '';
end;
$$;
-------------------------------------------------------------------------------
-- Public functions (in current schema, not basic_auth)
create or replace function
request_password_reset(email text) returns void
language plpgsql
as $$
declare
tok uuid;
begin
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = request_password_reset.email;
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',
json_build_object(
'email', request_password_reset.email,
'token', tok,
'token_type', 'reset'
)::text
);
end;
$$;
create or replace function
reset_password(email text, token uuid, pass text)
returns void
language plpgsql
as $$
declare
tok uuid;
begin
if exists(select 1 from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset') then
update basic_auth.users set pass=reset_password.pass
where users.email = reset_password.email;
delete from basic_auth.tokens
where tokens.email = reset_password.email
and tokens.token = reset_password.token
and token_type = 'reset';
else
raise invalid_password using message =
'invalid user or token';
end if;
delete from basic_auth.tokens
where token_type = 'reset'
and tokens.email = reset_password.email;
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',
json_build_object(
'email', reset_password.email,
'token', tok
)::text
);
end;
$$;
drop type if exists basic_auth.jwt_claims cascade;
create type
basic_auth.jwt_claims AS (role text, email text);
create or replace function
login(email text, pass text) returns basic_auth.jwt_claims
language plpgsql
as $$
declare
_role name;
result basic_auth.jwt_claims;
begin
select basic_auth.user_role(email, pass) into _role;
if _role is null then
raise invalid_password using message = 'invalid user or password';
end if;
-- TODO; check verified flag if you care whether users
-- have validated their emails
select _role as role, login.email as email into result;
return result;
end;
$$;
create or replace function
signup(email text, pass text) returns void
as $$
insert into basic_auth.users (email, pass, role) values
(signup.email, signup.pass, 'author');
$$ language sql;
-------------------------------------------------------------------------------
-- User management
create or replace view users as
select actual.role as role,
'***'::text as pass,
actual.email as email,
actual.verified as verified
from basic_auth.users as actual,
(select rolname
from pg_authid
where pg_has_role(current_user, oid, 'member')
) as member_of
where actual.role = member_of.rolname
and (
actual.role <> 'author'
or email = basic_auth.current_email()
);
create or replace function
update_users() returns trigger
language plpgsql
AS $$
begin
if tg_op = 'INSERT' then
perform basic_auth.clearance_for_role(new.role);
insert into basic_auth.users
(role, pass, email, verified) values
(coalesce(new.role, 'author'), new.pass,
new.email, coalesce(new.verified, false));
return new;
elsif tg_op = 'UPDATE' then
-- no need to check clearance for old.role because
-- an ineligible row would not even available to update (http 404)
perform basic_auth.clearance_for_role(new.role);
update basic_auth.users set
email = new.email,
role = new.role,
pass = new.pass,
verified = coalesce(new.verified, old.verified, false)
where email = old.email;
return new;
elsif tg_op = 'DELETE' then
-- no need to check clearance for old.role (see previous case)
delete from basic_auth.users
where basic_auth.email = old.email;
return null;
end if;
end
$$;
drop trigger if exists update_users on users;
create trigger update_users
instead of insert or update or delete on
users for each row execute procedure update_users();
-------------------------------------------------------------------------------
-- Blogging stuff!
create table if not exists
posts (
id bigserial primary key,
title text not null,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
created_at timestamptz not null default current_date
);
create table if not exists
comments (
id bigserial primary key,
body text not null,
author text not null references basic_auth.users (email)
on delete restrict on update cascade
default basic_auth.current_email(),
post bigint not null references posts (id)
on delete cascade on update cascade,
created_at timestamptz not null default current_date
);
-------------------------------------------------------------------------------
-- Permissions
grant insert on table basic_auth.users, basic_auth.tokens to anon;
grant select on table pg_authid, basic_auth.users, posts, comments to anon;
grant execute on function
login(text,text),
request_password_reset(text),
reset_password(text,uuid,text),
signup(text, text)
to anon;
grant select, insert, update, delete
on basic_auth.tokens, basic_auth.users to anon, author;
grant select, insert, update, delete
on table users, posts, comments to author;
grant usage, select on sequence posts_id_seq, comments_id_seq to author;
grant usage on schema public, basic_auth to anon, author;
ALTER TABLE posts ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on posts;
create policy authors_eigenedit on posts
using (true)
with check (
author = basic_auth.current_email()
);
ALTER TABLE comments ENABLE ROW LEVEL SECURITY;
drop policy if exists authors_eigenedit on comments;
create policy authors_eigenedit on comments
using (true)
with check (
author = basic_auth.current_email()
);
commit;
+1 -1
View File
@@ -1,6 +1,6 @@
export POSTGREST_VER=`grep ^version /app/postgrest.cabal | sed -En 's/.*\s+([0-9\.]+)/\1/p'`
curl -L http://softlayer-ams.dl.sourceforge.net/project/s3tools/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
curl -L http://sourceforge.net/projects/s3tools/files/s3cmd/1.5.0-alpha1/s3cmd-1.5.0-alpha1.tar.gz | tar zx
cp /app/dist/build/postgrest/postgrest postgrest-${POSTGREST_VER}
tar cJf postgrest-${POSTGREST_VER}.tar.xz postgrest-${POSTGREST_VER}
-238
View File
@@ -1,238 +0,0 @@
{-# LANGUAGE FlexibleContexts #-}
module App (app, sqlError, isSqlError) where
import Control.Monad (join)
import Control.Arrow ((***))
import Control.Applicative
import Data.Text hiding (map)
import Data.Maybe (fromMaybe)
import Text.Regex.TDFA ((=~))
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange)
import Data.HashMap.Strict (keys, elems, filterWithKey, toList)
import Data.String.Conversions (cs)
import Data.List (sortBy)
import Data.Functor.Identity
import qualified Data.Set as S
import qualified Data.ByteString.Lazy as BL
import Network.HTTP.Types.Status
import Network.HTTP.Types.Header
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.HTTP.Base (urlEncodeVars)
import Network.Wai
import Data.Aeson
import Data.Monoid
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import Auth
import PgQuery
import RangeQuery
import PgStructure
app :: BL.ByteString -> Request -> H.Tx P.Postgres s Response
app reqBody req =
case (path, verb) of
([], _) -> do
body <- encode <$> tables (cs schema)
return $ responseLBS status200 [jsonH] $ cs body
([table], "OPTIONS") -> do
let t = QualifiedTable schema (cs table)
cols <- columns t
pkey <- map cs <$> primaryKeyColumns t
return $ responseLBS status200 [jsonH, allOrigins]
$ encode (TableOptions cols pkey)
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else do
let qt = QualifiedTable schema (cs table)
let select = B.Stmt "select " V.empty True <>
parentheticT (
whereT qq $ countRows qt
) <> commaq <> (
asJsonWithCount
. limitT range
. orderT (orderParse qq)
. whereT qq
$ selectStar qt
)
row <- H.maybeEx select
let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row
from = fromMaybe 0 $ rangeOffset <$> range
to = from+queryTotal-1
contentRange = contentRangeH from to tableTotal
status = rangeStatus from to tableTotal
canonical = urlEncodeVars
. sortBy (comparing fst)
. map (join (***) cs)
. parseSimpleQuery
$ rawQueryString req
return $ responseLBS status
[jsonH, contentRange,
("Content-Location",
"/" <> cs table <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (cs $ fromMaybe "[]" body)
(["postgrest", "users"], "POST") -> do
let user = decode reqBody :: Maybe AuthUser
case user of
Nothing -> return $ responseLBS status400 [jsonH] $
encode . object $ [("message", String "Failed to parse user.")]
Just u -> do
_ <- addUser (cs $ userId u)
(cs $ userPass u) (cs $ userRole u)
return $ responseLBS status201
[ jsonH
, (hLocation, "/postgrest/users?id=eq." <> cs (userId u))
] ""
([table], "POST") ->
handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table)
query = insertInto qt (map cs $ keys obj) (elems obj)
row <- H.maybeEx query
let (Identity insertedJson) = fromMaybe (Identity "{}" :: Identity Text) row
Just inserted = decode (cs insertedJson) :: Maybe Object
primaryKeys <- map cs <$> primaryKeyColumns qt
let primaries = if Prelude.null primaryKeys
then inserted
else filterWithKey (const . (`elem` primaryKeys)) inserted
let params = urlEncodeVars
$ map (\t -> (cs $ fst t, "eq." <> cs (unquoted $ snd t)))
$ sortBy (comparing fst) $ toList primaries
return $ responseLBS status201
[ jsonH
, (hLocation, "/" <> cs table <> "?" <> cs params)
] ""
([table], "PUT") ->
handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table)
primaryKeys <- primaryKeyColumns qt
let specifiedKeys = map (cs . fst) qq
if S.fromList primaryKeys /= S.fromList specifiedKeys
then return $ responseLBS status405 []
"You must speficy all and only primary keys as params"
else do
tableCols <- map (cs . colName) <$> columns qt
let cols = map cs $ keys obj
if S.fromList tableCols == S.fromList cols
then do
let vals = elems obj
H.unitEx $ iffNotT
(whereT qq $ update qt cols vals)
(insertSelect qt cols vals)
return $ responseLBS status204 [ jsonH ] ""
else return $ if Prelude.null tableCols
then responseLBS status404 [] ""
else responseLBS status400 []
"You must specify all columns in PUT request"
([table], "PATCH") ->
handleJsonObj reqBody $ \obj -> do
let qt = QualifiedTable schema (cs table)
H.unitEx
$ whereT qq
$ update qt (map cs $ keys obj) (elems obj)
return $ responseLBS status204 [ jsonH ] ""
([table], "DELETE") -> do
let qt = QualifiedTable schema (cs table)
let del = countT
. returningStarT
. whereT qq
$ deleteFrom qt
row <- H.maybeEx del
let (Identity deletedCount) = fromMaybe (Identity 0 :: Identity Int) row
return $ if deletedCount == 0
then responseLBS status404 [] ""
else responseLBS status204 [("Content-Range", "*/"<> cs (show deletedCount))] ""
(_, _) ->
return $ responseLBS status404 [] ""
where
path = pathInfo req
verb = requestMethod req
qq = queryString req
hdrs = requestHeaders req
schema = requestedSchema hdrs
range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
sqlError :: t
sqlError = undefined
isSqlError :: t
isSqlError = undefined
rangeStatus :: Int -> Int -> Int -> Status
rangeStatus from to total
| from > total = status416
| (1 + to - from) < total = status206
| otherwise = status200
contentRangeH :: Int -> Int -> Int -> Header
contentRangeH from to total =
("Content-Range",
if total == 0 || from > total
then "*/" <> cs (show total)
else cs (show from) <> "-"
<> cs (show to) <> "/"
<> cs (show total)
)
requestedSchema :: RequestHeaders -> Text
requestedSchema hdrs =
case verStr of
Just [[_, ver]] -> ver
_ -> "1"
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: String
accept = cs <$> lookup hAccept hdrs :: Maybe Text
verStr = (=~ verRegex) <$> accept :: Maybe [[Text]]
jsonH :: Header
jsonH = (hContentType, "application/json")
handleJsonObj :: BL.ByteString -> (Object -> H.Tx P.Postgres s Response)
-> H.Tx P.Postgres s Response
handleJsonObj reqBody handler = do
let p = eitherDecode reqBody
case p of
Left err ->
return $ responseLBS status400 [jsonH] jErr
where
jErr = encode . object $
[("message", String $ "Failed to parse JSON payload. " <> cs err)]
Right (Object o) -> handler o
Right _ ->
return $ responseLBS status400 [jsonH] jErr
where
jErr = encode . object $
[("message", String "Expecting a JSON object")]
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [Text]
}
instance ToJSON TableOptions where
toJSON t = object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
-71
View File
@@ -1,71 +0,0 @@
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables, OverloadedStrings #-}
module Auth where
import Data.Aeson
import Control.Monad (mzero)
import Control.Applicative ( (<*>), (<$>) )
import Crypto.BCrypt
import Data.Text
import Data.Monoid
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import Data.String.Conversions (cs)
import PgQuery (pgFmtLit)
import System.IO.Unsafe
data AuthUser = AuthUser {
userId :: String
, userPass :: String
, userRole :: String
} deriving (Show)
instance FromJSON AuthUser where
parseJSON (Object v) = AuthUser <$>
v .: "id" <*>
v .: "pass" <*>
v .: "role"
parseJSON _ = mzero
instance ToJSON AuthUser where
toJSON u = object [
"id" .= userId u
, "pass" .= userPass u
, "role" .= userRole u ]
type DbRole = Text
data LoginAttempt =
NoCredentials
| MalformedAuth
| LoginFailed
| LoginSuccess DbRole
deriving (Eq, Show)
checkPass :: Text -> Text -> Bool
checkPass = (. cs) . validatePassword . cs
setRole :: Text -> H.Tx P.Postgres s ()
setRole role = H.unitEx $ B.Stmt ("set role " <> cs (pgFmtLit role)) V.empty True
resetRole :: H.Tx P.Postgres s ()
resetRole = H.unitEx [H.stmt|reset role|]
addUser :: Text -> Text -> Text -> H.Tx P.Postgres s ()
addUser identity pass role = do
let Just hashed = unsafePerformIO $ hashPasswordUsingPolicy fastBcryptHashingPolicy (cs pass)
H.unitEx $
[H.stmt|insert into postgrest.auth (id, pass, rolname) values (?, ?, ?)|]
identity (cs hashed :: Text) role
signInRole :: Text -> Text -> H.Tx P.Postgres s LoginAttempt
signInRole user pass = do
u <- H.maybeEx $ [H.stmt|select pass, rolname from postgrest.auth where id = ?|] user
return $ maybe LoginFailed (\r ->
let (hashed, role) = r in
if checkPass hashed pass
then LoginSuccess role
else LoginFailed
) u
-58
View File
@@ -1,58 +0,0 @@
module Config where
import Network.Wai
import Control.Applicative
import Data.Text (strip)
import qualified Data.CaseInsensitive as CI
import qualified Data.ByteString.Char8 as BS
import Data.String.Conversions (cs)
import Options.Applicative hiding (columns)
import Network.Wai.Middleware.Cors (CorsResourcePolicy(..))
data AppConfig = AppConfig {
configDbName :: String
, configDbPort :: Int
, configDbUser :: String
, configDbPass :: String
, configDbHost :: String
, configPort :: Int
, configAnonRole :: String
, configSecure :: Bool
, configPool :: Int
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> help "name of database")
<*> option auto (long "db-port" <> short 'P' <> metavar "PORT" <> value 5432 <> help "postgres server port" <> showDefault)
<*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> help "postgres authenticator role")
<*> strOption (long "db-pass" <> metavar "PASS" <> value "" <> help "password for authenticator role")
<*> strOption (long "db-host" <> metavar "HOST" <> value "localhost" <> help "postgres server hostname" <> showDefault)
<*> option auto (long "port" <> short 'p' <> metavar "PORT" <> value 3000 <> help "port number on which to run HTTP server" <> showDefault)
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
<*> option auto (long "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
corsPolicy :: Request -> Maybe CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders
, corsExposedHeaders = Just [
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
]
}
Nothing -> Nothing
where
headers = requestHeaders req
accHeaders = case lookup "access-control-request-headers" headers of
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
Nothing -> []
-71
View File
@@ -1,71 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
module Error (PgError, errResponse) where
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Network.HTTP.Types.Status as HT
import qualified Data.Aeson as JSON
import qualified Data.Text as T
import Data.Aeson ((.=))
import Data.String.Conversions (cs)
import Data.String.Utils(replace)
import Network.Wai(Response, responseLBS)
import Network.HTTP.Types.Header
type PgError = H.SessionError P.Postgres
errResponse :: PgError -> Response
errResponse e = responseLBS (httpStatus e)
[(hContentType, "application/json")] (JSON.encode e)
instance JSON.ToJSON PgError where
toJSON (H.TxError (P.ErroneousResult c m d h)) = JSON.object [
"code" .= (cs c::T.Text),
"message" .= (cs m::T.Text),
"details" .= (fmap cs d::Maybe T.Text),
"hint" .= (fmap cs h::Maybe T.Text)]
toJSON (H.TxError (P.NoResult d)) = JSON.object [
"message" .= ("No response from server"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.TxError (P.UnexpectedResult m)) = JSON.object ["message" .= m]
toJSON (H.TxError P.NotInTransaction) = JSON.object [
"message" .= ("Not in transaction"::T.Text)]
toJSON (H.CxError (P.CantConnect d)) = JSON.object [
"message" .= ("Can't connect to the database"::T.Text),
"details" .= (fmap cs d::Maybe T.Text)]
toJSON (H.CxError (P.UnsupportedVersion v)) = JSON.object [
"message" .= ("Postgres version "++version++" is not supported") ]
where version = replace "0" "." (show v)
toJSON (H.ResultError m) = JSON.object ["message" .= m]
httpStatus :: PgError -> HT.Status
httpStatus (H.TxError (P.ErroneousResult codeBS _ _ _)) =
let code = cs codeBS in
case code of
'0':'8':_ -> HT.status503 -- pg connection err
'0':'9':_ -> HT.status500 -- triggered action exception
'0':'L':_ -> HT.status403 -- invalid grantor
'0':'P':_ -> HT.status403 -- invalid role specification
'2':'5':_ -> HT.status500 -- invalid tx state
'2':'8':_ -> HT.status403 -- invalid auth specification
'2':'D':_ -> HT.status500 -- invalid tx termination
'3':'8':_ -> HT.status500 -- external routine exception
'3':'9':_ -> HT.status500 -- external routine invocation
'3':'B':_ -> HT.status500 -- savepoint exception
'4':'0':_ -> HT.status500 -- tx rollback
'5':'3':_ -> HT.status503 -- insufficient resources
'5':'4':_ -> HT.status413 -- too complex
'5':'5':_ -> HT.status500 -- obj not on prereq state
'5':'7':_ -> HT.status500 -- operator intervention
'5':'8':_ -> HT.status500 -- system error
'F':'0':_ -> HT.status500 -- conf file error
'H':'V':_ -> HT.status500 -- foreign data wrapper error
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
'X':'X':_ -> HT.status500 -- internal Error
"42P01" -> HT.status404 -- undefined table
"42501" -> HT.status404 -- insufficient privilege
_ -> HT.status400
httpStatus (H.TxError (P.NoResult _)) = HT.status503
httpStatus _ = HT.status500
-69
View File
@@ -1,69 +0,0 @@
module Main where
import Paths_postgrest (version)
import App
import Middleware
import Error(errResponse)
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Data.String.Conversions (cs)
import Network.Wai (strictRequestBody)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.Gzip (gzip, def)
import Network.Wai.Middleware.Static (staticPolicy, only)
import Data.List (intercalate)
import Data.Version (versionBranch)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Options.Applicative hiding (columns)
import Config (AppConfig(..), argParser, corsPolicy)
main :: IO ()
main = do
let opts = info (helper <*> argParser) $
fullDesc
<> progDesc (
"PostgREST "
<> prettyVersion
<> " / create a REST API to an existing Postgres database"
)
conf <- execParser opts
let port = configPort conf
unless (configSecure conf) $
putStrLn "WARNING, running in insecure mode, auth will be in plaintext"
Prelude.putStrLn $ "Listening on port " ++
(show $ configPort conf :: String)
let pgSettings = P.ParamSettings (cs $ configDbHost conf)
(fromIntegral $ configDbPort conf)
(cs $ configDbUser conf)
(cs $ configDbPass conf)
(cs $ configDbName conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle =
(if configSecure conf then redirectInsecure else id)
. gzip def . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
anonRole = cs $ configAnonRole conf
currRole = cs $ configDbUser conf
poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings poolSettings
runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx Nothing $
authenticated currRole anonRole (app body) req
either (respond . errResponse) respond resOrError
where
prettyVersion = intercalate "." $ map show $ versionBranch version
-76
View File
@@ -1,76 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
module Middleware where
import Data.Maybe (fromMaybe)
import Data.Monoid (mconcat)
import Data.Text
-- import Data.Pool(withResource, Pool)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Data.String.Conversions(cs)
import Network.HTTP.Types.Header (hLocation, hAuthorization)
import Network.HTTP.Types (RequestHeaders)
import Network.HTTP.Types.Status (status400, status401, status301)
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
rawQueryString, isSecure, Request(..), Response)
import Network.URI (URI(..), parseURI)
import Auth (LoginAttempt(..), signInRole, setRole, resetRole)
import Codec.Binary.Base64.String (decode)
authenticated :: forall s. Text -> Text ->
(Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response
authenticated currentRole anon app req = do
attempt <- httpRequesterRole (requestHeaders req)
case attempt of
MalformedAuth ->
return $ responseLBS status400 [] "Malformed basic auth header"
LoginFailed ->
return $ responseLBS status401 [] "Invalid username or password"
LoginSuccess role -> if role /= currentRole then runInRole role else app req
NoCredentials -> if anon /= currentRole then runInRole anon else app req
where
httpRequesterRole :: RequestHeaders -> H.Tx P.Postgres s LoginAttempt
httpRequesterRole hdrs = do
let auth = fromMaybe "" $ lookup hAuthorization hdrs
case split (==' ') (cs auth) of
("Basic" : b64 : _) ->
case split (==':') (cs . decode . cs $ b64) of
(u:p:_) -> signInRole u p
_ -> return MalformedAuth
_ -> return NoCredentials
runInRole :: Text -> H.Tx P.Postgres s Response
runInRole r = do
setRole r
res <- app req
resetRole
return res
redirectInsecure :: Application -> Application
redirectInsecure app req respond = do
let hdrs = requestHeaders req
host = lookup "host" hdrs
uriM = parseURI . cs =<< mconcat [
Just "https://",
host,
Just $ rawPathInfo req,
Just $ rawQueryString req]
isHerokuSecure = lookup "x-forwarded-proto" hdrs == Just "https"
if not (isSecure req || isHerokuSecure)
then case uriM of
Just uri ->
respond $ responseLBS status301 [
(hLocation, cs . show $ uri { uriScheme = "https:" })
] ""
Nothing ->
respond $ responseLBS status400 [] "SSL is required"
else app req respond
-212
View File
@@ -1,212 +0,0 @@
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module PgQuery where
import RangeQuery
import qualified Hasql as H
import qualified Hasql.Postgres as P
import qualified Hasql.Backend as B
import qualified Data.Text as T
import Text.Regex.TDFA ( (=~) )
import Text.Regex.TDFA.Text ()
import qualified Network.HTTP.Types.URI as Net
import qualified Data.ByteString.Char8 as BS
import Data.Monoid
import Data.Vector (empty)
import Data.Maybe (fromMaybe, mapMaybe)
import Data.Functor ( (<$>) )
import Control.Monad (join)
import Data.String.Conversions (cs)
import qualified Data.Aeson as JSON
import qualified Data.List as L
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
type PStmt = H.Stmt P.Postgres
instance Monoid PStmt where
mappend (B.Stmt query params prep) (B.Stmt query' params' prep') =
B.Stmt (query <> query') (params <> params') (prep && prep')
mempty = B.Stmt "" empty True
type StatementT = PStmt -> PStmt
data QualifiedTable = QualifiedTable {
qtSchema :: T.Text
, qtName :: T.Text
} deriving (Show)
data OrderTerm = OrderTerm {
otTerm :: T.Text
, otDirection :: BS.ByteString
}
limitT :: Maybe NonnegRange -> StatementT
limitT r q =
q <> B.Stmt (" LIMIT " <> limit <> " OFFSET " <> offset <> " ") empty True
where
limit = maybe "ALL" (cs . show) $ join $ rangeLimit <$> r
offset = cs . show $ fromMaybe 0 $ rangeOffset <$> r
whereT :: Net.Query -> StatementT
whereT params q =
if L.null cols
then q
else q <> B.Stmt " where " empty True <> conjunction
where
cols = [ col | col <- params, fst col `notElem` ["order"] ]
conjunction = mconcat $ L.intersperse andq (map wherePred cols)
orderT :: [OrderTerm] -> StatementT
orderT ts q =
if L.null ts
then q
else q <> B.Stmt " order by " empty True <> clause
where
clause = mconcat $ L.intersperse commaq (map queryTerm ts)
queryTerm :: OrderTerm -> PStmt
queryTerm t = B.Stmt
(" " <> cs (pgFmtIdent $ otTerm t) <> " "
<> cs (otDirection t) <> " ")
empty True
parentheticT :: StatementT
parentheticT s =
s { B.stmtTemplate = " (" <> B.stmtTemplate s <> ") " }
iffNotT :: PStmt -> StatementT
iffNotT (B.Stmt aq ap apre) (B.Stmt bq bp bpre) =
B.Stmt
("WITH aaa AS (" <> aq <> " returning *) " <>
bq <> " WHERE NOT EXISTS (SELECT * FROM aaa)")
(ap <> bp)
(apre && bpre)
countT :: StatementT
countT s =
s { B.stmtTemplate = "WITH qqq AS (" <> B.stmtTemplate s <> ") SELECT count(1) FROM qqq" }
countRows :: QualifiedTable -> PStmt
countRows t = B.Stmt ("select count(1) from " <> fromQt t) empty True
asJsonWithCount :: StatementT
asJsonWithCount s = s { B.stmtTemplate =
"count(t), array_to_json(array_agg(row_to_json(t)))::character varying from ("
<> B.stmtTemplate s <> ") t" }
asJsonRow :: StatementT
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
selectStar :: QualifiedTable -> PStmt
selectStar t = B.Stmt ("select * from " <> fromQt t) empty True
returningStarT :: StatementT
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
deleteFrom :: QualifiedTable -> PStmt
deleteFrom t = B.Stmt ("delete from " <> fromQt t) empty True
insertInto :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
insertInto t [] _ = B.Stmt
("insert into " <> fromQt t <> " default values returning *") empty True
insertInto t cols vals = B.Stmt
("insert into " <> fromQt t <> " (" <>
T.intercalate ", " (map pgFmtIdent cols) <>
") values ("
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
<> ") returning row_to_json(" <> fromQt t <> ".*)")
empty True
insertSelect :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
insertSelect t [] _ = B.Stmt
("insert into " <> fromQt t <> " default values returning *") empty True
insertSelect t cols vals = B.Stmt
("insert into " <> fromQt t <> " ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") select "
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals))
empty True
update :: QualifiedTable -> [T.Text] -> [JSON.Value] -> PStmt
update t cols vals = B.Stmt
("update " <> fromQt t <> " set ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") = ("
<> T.intercalate ", " (map ((<> "::unknown") . pgFmtLit . unquoted) vals)
<> ")")
empty True
wherePred :: Net.QueryItem -> PStmt
wherePred (col, predicate) = B.Stmt
(" " <> cs (pgFmtIdent $ cs col) <> " " <> op <> " " <> cs (pgFmtLit value) <> "::unknown ")
empty True
where
opCode:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
unStarredVal = T.intercalate "." rest
star c = if c == '*' then '%' else c
value = if opCode == "like" || opCode == "ilike"
then T.map star unStarredVal else unStarredVal
op = case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
_ -> "="
orderParse :: Net.Query -> [OrderTerm]
orderParse q =
mapMaybe orderParseTerm . T.split (==',') $ cs order
where
order = fromMaybe "" $ join (lookup "order" q)
orderParseTerm :: T.Text -> Maybe OrderTerm
orderParseTerm s =
case T.split (=='.') s of
[c,d] ->
if d `elem` ["asc", "desc"]
then Just $ OrderTerm c $
if d == "asc" then "asc" else "desc"
else Nothing
_ -> Nothing
commaq :: PStmt
commaq = B.Stmt ", " empty True
andq :: PStmt
andq = B.Stmt " and " empty True
pgFmtIdent :: T.Text -> T.Text
pgFmtIdent x =
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
if escaped =~ danger
then "\"" <> escaped <> "\""
else escaped
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: T.Text
pgFmtLit :: T.Text -> T.Text
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
slashed = T.replace "\\" "\\\\" escaped in
cs $ if escaped =~ ("\\\\" :: T.Text)
then "E" <> slashed
else slashed
trimNullChars :: T.Text -> T.Text
trimNullChars = T.takeWhile (/= '\x0')
fromQt :: QualifiedTable -> T.Text
fromQt t = pgFmtIdent (qtSchema t) <> "." <> pgFmtIdent (qtName t)
unquoted :: JSON.Value -> T.Text
unquoted (JSON.String t) = t
unquoted (JSON.Number n) =
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
unquoted (JSON.Bool b) = cs . show $ b
unquoted _ = ""
-172
View File
@@ -1,172 +0,0 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings, TypeSynonymInstances,
MultiParamTypeClasses, ScopedTypeVariables #-}
module PgStructure where
import PgQuery (QualifiedTable(..))
import Data.Text hiding (foldl, map, zipWith, concat)
import Data.Aeson
import Data.Functor.Identity
import Data.String.Conversions (cs)
import Data.Maybe (fromMaybe)
import Control.Applicative ( (<$>) )
import qualified Data.Map as Map
import qualified Hasql as H
import qualified Hasql.Postgres as P
foreignKeys :: QualifiedTable -> H.Tx P.Postgres s (Map.Map Text ForeignKey)
foreignKeys table = do
r <- H.listEx $ [H.stmt|
select kcu.column_name, ccu.table_name AS foreign_table_name,
ccu.column_name AS foreign_column_name
from information_schema.table_constraints AS tc
join information_schema.key_column_usage AS kcu
on tc.constraint_name = kcu.constraint_name
join information_schema.constraint_column_usage AS ccu
on ccu.constraint_name = tc.constraint_name
where constraint_type = 'FOREIGN KEY'
and tc.table_name=? and tc.table_schema = ?
order by kcu.column_name
|] (qtName table) (qtSchema table)
return $ foldl addKey Map.empty r
where
addKey :: Map.Map Text ForeignKey -> (Text, Text, Text) -> Map.Map Text ForeignKey
addKey m (col, ftab, fcol) = Map.insert col (ForeignKey ftab fcol) m
tables :: Text -> H.Tx P.Postgres s [Table]
tables schema = do
rows <- H.listEx $
[H.stmt|
select table_schema, table_name,
is_insertable_into
from information_schema.tables
where table_schema = ?
order by table_name
|] schema
return $ map tableFromRow rows
columns :: QualifiedTable -> H.Tx P.Postgres s [Column]
columns table = do
cols <- H.listEx $ [H.stmt|
select info.table_schema as schema, info.table_name as table_name,
info.column_name as name, info.ordinal_position as position,
info.is_nullable as nullable, info.data_type as col_type,
info.is_updatable as updatable,
info.character_maximum_length as max_len,
info.numeric_precision as precision,
info.column_default as default_value,
array_to_string(enum_info.vals, ',') as enum
from (
select table_schema, table_name, column_name, ordinal_position,
is_nullable, data_type, is_updatable,
character_maximum_length, numeric_precision,
column_default, udt_name
from information_schema.columns
where table_schema = ? and table_name = ?
) as info
left outer join (
select n.nspname as s,
t.typname as n,
array_agg(e.enumlabel ORDER BY e.enumsortorder) as vals
from pg_type t
join pg_enum e on t.oid = e.enumtypid
join pg_catalog.pg_namespace n ON n.oid = t.typnamespace
group by s, n
) as enum_info
on (info.udt_name = enum_info.n)
order by position |]
(qtSchema table) (qtName table)
fks <- foreignKeys table
return $ map (addFK fks . columnFromRow) cols
where
addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks }
primaryKeyColumns :: QualifiedTable -> H.Tx P.Postgres s [Text]
primaryKeyColumns table = do
r <- H.listEx $ [H.stmt|
select kc.column_name
from
information_schema.table_constraints tc,
information_schema.key_column_usage kc
where
tc.constraint_type = 'PRIMARY KEY'
and kc.table_name = tc.table_name and kc.table_schema = tc.table_schema
and kc.constraint_name = tc.constraint_name
and kc.table_schema = ?
and kc.table_name = ? |] (qtSchema table) (qtName table)
return $ map runIdentity r
toBool :: Text -> Bool
toBool = (== "YES")
data Table = Table {
tableSchema :: Text
, tableName :: Text
, tableInsertable :: Bool
} deriving (Show)
data ForeignKey = ForeignKey {
fkTable::Text, fkCol::Text
} deriving (Eq, Show)
data Column = Column {
colSchema :: Text
, colTable :: Text
, colName :: Text
, colPosition :: Int
, colNullable :: Bool
, colType :: Text
, colUpdatable :: Bool
, colMaxLen :: Maybe Int
, colPrecision :: Maybe Int
, colDefault :: Maybe Text
, colEnum :: [Text]
, colFK :: Maybe ForeignKey
} deriving (Show)
tableFromRow :: (Text, Text, Text) -> Table
tableFromRow (s, n, i) = Table s n (toBool i)
columnFromRow :: (Text, Text, Text,
Int, Text, Text,
Text, Maybe Int, Maybe Int,
Maybe Text, Maybe Text)
-> Column
columnFromRow (s, t, n, pos, nul, typ, u, l, p, d, e) =
Column s t n pos (toBool nul) typ (toBool u) l p d (parseEnum e) Nothing
where
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
instance ToJSON Column where
toJSON c = object [
"schema" .= colSchema c
, "name" .= colName c
, "position" .= colPosition c
, "nullable" .= colNullable c
, "type" .= colType c
, "updatable" .= colUpdatable c
, "maxLen" .= colMaxLen c
, "precision" .= colPrecision c
, "references".= colFK c
, "default" .= colDefault c
, "enum" .= colEnum c ]
instance ToJSON ForeignKey where
toJSON fk = object ["table".=fkTable fk, "column".=fkCol fk]
instance ToJSON Table where
toJSON v = object [
"schema" .= tableSchema v
, "name" .= tableName v
, "insertable" .= tableInsertable v ]
+232
View File
@@ -0,0 +1,232 @@
module PostgREST.ApiRequest where
import qualified Data.Aeson as JSON
import qualified Data.ByteString as BS
import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as CSV
import Data.List (find)
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import Data.Maybe (fromMaybe, isJust, isNothing,
listToMaybe, fromJust)
import Control.Monad (join)
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
import qualified Data.Text as T
import qualified Data.Vector as V
import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept)
import PostgREST.RangeQuery (NonnegRange, rangeRequested)
import PostgREST.Types (QualifiedIdentifier (..),
Schema, Payload(..),
UniformObjects(..))
import Data.Ranged.Ranges (singletonRange)
type RequestBody = BL.ByteString
-- | Types of things a user wants to do to tables/views/procs
data Action = ActionCreate | ActionRead
| ActionUpdate | ActionDelete
| ActionInfo | ActionInvoke
| 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
data PreferRepresentation = Full | HeadersOnly | None deriving Eq
-- | Enumeration of currently supported content types for
-- route responses and upload payloads
data ContentType = ApplicationJSON | TextCSV deriving Eq
instance Show ContentType where
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
translation of the raw elements of an HTTP request into domain
specific language. There is no guarantee that the intent is
sensible, it is up to a later stage of processing to determine
if it is an action we are able to perform.
-}
data ApiRequest = ApiRequest {
-- | Set to Nothing for unknown HTTP verbs
iAction :: Action
-- | Set to Nothing for malformed range
, iRange :: NonnegRange
-- | Set to Nothing for strangely nested urls
, iTarget :: Target
-- | The content type the client most desires (or JSON if undecided)
, iAccepts :: Either BS.ByteString ContentType
-- | Data sent by client and used for mutation actions
, iPayload :: Maybe Payload
-- | If client wants created items echoed back
, iPreferRepresentation :: PreferRepresentation
-- | If client wants first row as raw object
, iPreferSingular :: Bool
-- | Whether the client wants a result count (slower)
, iPreferCount :: Bool
-- | Filters on the result ("id", "eq.10")
, iFilters :: [(String, String)]
-- | &select parameter used to shape the response
, iSelect :: String
-- | &order parameter
, iOrder :: Maybe String
}
-- | Examines HTTP request and translates it into user intent.
userApiRequest :: Schema -> Request -> RequestBody -> ApiRequest
userApiRequest schema req reqBody =
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] -> TargetProc
$ QualifiedIdentifier schema proc
other -> TargetUnknown other
payload = case pickContentType (lookupHeader "content-type") of
Right ApplicationJSON ->
either (PayloadParseError . cs)
(\val -> case ensureUniform (pluralize val) of
Nothing -> PayloadParseError "All object keys must match"
Just json -> PayloadJSON json)
(JSON.eitherDecode reqBody)
Right TextCSV ->
either (PayloadParseError . cs)
(\val -> case ensureUniform (csvToJson val) of
Nothing -> PayloadParseError "All lines must have same number of fields"
Just json -> PayloadJSON json)
(CSV.decodeByName reqBody)
Left accept ->
PayloadParseError $
"Content-type not acceptable: " <> accept
relevantPayload = case action of
ActionCreate -> Just payload
ActionUpdate -> Just payload
ActionInvoke -> Just payload
_ -> Nothing in
ApiRequest {
iAction = action
, iRange = if singular then singletonRange 0 else rangeRequested hdrs
, iTarget = target
, iAccepts = pickContentType $ lookupHeader "accept"
, iPayload = relevantPayload
, iPreferRepresentation = representation
, iPreferSingular = singular
, iPreferCount = not $ singular || hasPrefer "count=none"
, iFilters = [ (k, fromJust v) | (k,v) <- qParams, k `notElem` ["select", "order"], isJust v ]
, iSelect = if method == "DELETE"
then "*"
else fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
, iOrder = join $ lookup "order" qParams
}
where
path = pathInfo req
method = requestMethod req
isTargetingProc = fromMaybe False $ (== "rpc") <$> listToMaybe path
hdrs = requestHeaders req
qParams = [(cs k, cs <$> v)|(k,v) <- queryString req]
lookupHeader = flip lookup 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
| hasPrefer "return=minimal" = None
| otherwise = HeadersOnly
-- PRIVATE ---------------------------------------------------------------
{-|
Picks a preferred content type from an Accept header (or from
Content-Type as a degenerate case).
For example
text/csv -> TextCSV
*/* -> ApplicationJSON
text/csv, application/json -> TextCSV
application/json, text/csv -> ApplicationJSON
-}
pickContentType :: Maybe BS.ByteString -> Either BS.ByteString ContentType
pickContentType accept
| isNothing accept || has ctAll || has ctJson = Right ApplicationJSON
| has ctCsv = Right TextCSV
| otherwise = Left accept'
where
ctAll = "*/*"
ctCsv = "text/csv"
ctJson = "application/json"
Just accept' = accept
findInAccept = flip find $ parseHttpAccept accept'
has = isJust . findInAccept . BS.isPrefixOf
type CsvData = V.Vector (M.HashMap T.Text BL.ByteString)
{-|
Converts CSV like
a,b
1,hi
2,bye
into a JSON array like
[ {"a": "1", "b": "hi"}, {"a": 2, "b": "bye"} ]
The reason for its odd signature is so that it can compose
directly with CSV.decodeByName
-}
csvToJson :: (CSV.Header, CsvData) -> JSON.Array
csvToJson (_, vals) =
V.map rowToJsonObj vals
where
rowToJsonObj = JSON.Object .
M.map (\str ->
if str == "NULL"
then JSON.Null
else JSON.String $ cs str
)
-- | Convert {foo} to [{foo}], leave arrays unchanged
-- and truncate everything else to an empty array.
pluralize :: JSON.Value -> JSON.Array
pluralize obj@(JSON.Object _) = V.singleton obj
pluralize (JSON.Array arr) = arr
pluralize _ = V.empty
-- | Test that Array contains only Objects having the same keys
-- and if so mark it as UniformObjects
ensureUniform :: JSON.Array -> Maybe UniformObjects
ensureUniform arr =
let objs :: V.Vector JSON.Object
objs = foldr -- filter non-objects, map to raw objects
(\val result -> case val of
JSON.Object o -> V.cons o result
_ -> result)
V.empty arr
keysPerObj = V.map (S.fromList . M.keys) objs
canonicalKeys = fromMaybe S.empty $ keysPerObj V.!? 0
areKeysUniform = all (==canonicalKeys) keysPerObj in
if (V.length objs == V.length arr) && areKeysUniform
then Just (UniformObjects objs)
else Nothing
+362
View File
@@ -0,0 +1,362 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TupleSections #-}
--module PostgREST.App where
module PostgREST.App (
postgrest
) where
import Control.Applicative
import Control.Arrow ((***))
import Control.Monad (join)
import Data.Bifunctor (first)
import Data.List (find, sortBy, delete)
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)
import Network.HTTP.Base (urlEncodeVars)
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.Transaction as H
import PostgREST.ApiRequest (ApiRequest(..), ContentType(..)
, Action(..), Target(..)
, PreferRepresentation (..)
, userApiRequest)
import PostgREST.Auth (tokenJWT)
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
, requestToCountQuery
, addRelations
, createReadStatement
, createWriteStatement
, ResultsWithCount
)
import PostgREST.Types
import Prelude
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)
contentType = either (const ApplicationJSON) id (iAccepts apiRequest)
contentTypeH = (hContentType, cs $ show contentType) in
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
(ActionRead, TargetIdent qi, Nothing) ->
case readSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (q, cq) -> do
let singular = iPreferSingular apiRequest
stm = createReadStatement q cq range singular
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] (cs body)
else do
let (status, contentRange) = rangeHeader queryTotal tableTotal
canonical = urlEncodeVars -- should this be moved to the dbStructure (location)?
. sortBy (comparing fst)
. map (join (***) cs)
. parseSimpleQuery
$ rawQueryString req
return $ responseLBS status
[contentTypeH, contentRange,
("Content-Location",
"/" <> cs (qiName qi) <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (cs body)
(ActionCreate, TargetIdent qi@(QualifiedIdentifier _ table),
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.query uniform stm
let (_, _, location, body) = extractQueryResult row
return $ responseLBS status201
[
contentTypeH,
(hLocation, "/" <> cs table <> "?" <> cs location)
]
$ if iPreferRepresentation apiRequest == Full then cs body else ""
(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.query uniform stm
let (_, queryTotal, _, body) = extractQueryResult row
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 cs body else ""
(ActionDelete, TargetIdent qi, Nothing) ->
case mutateSqlParts of
Left e -> return $ responseLBS status400 [jsonH] $ cs e
Right (sq,mq) -> do
let emptyUniform = UniformObjects V.empty
let fakeload = PayloadJSON emptyUniform
let stm = createWriteStatement qi sq mq False (iPreferRepresentation apiRequest) [] (contentType == TextCSV) fakeload
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) ->
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, TargetProc qi,
Just (PayloadJSON (UniformObjects payload))) -> do
exists <- H.query qi doesProcExist
if exists
then do
let p = V.head payload
jwtSecret = configJwtSecret conf
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 <$> H.query schema accessibleTables
return $ responseLBS status200 [jsonH] $ cs body
(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
notFound = responseLBS status404 [] ""
filterPk sc table pk = sc == (tableSchema . pkTable) pk && table == (tableName . pkTable) pk
allPrKeys = dbPrimaryKeys dbStructure
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
countQuery = requestToCountQuery schema <$> readDbRequest
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 :: Integer -> Integer -> Maybe Integer -> Status
rangeStatus _ _ Nothing = status200
rangeStatus frm to (Just total)
| frm > total = status416
| (1 + to - frm) < total = status206
| otherwise = status200
contentRangeH :: Integer -> Integer -> Maybe Integer -> Header
contentRangeH frm to total =
("Content-Range", cs headerValue)
where
headerValue = rangeString <> "/" <> totalString
rangeString
| totalNotZero && fromInRange = show frm <> "-" <> cs (show to)
| otherwise = "*"
totalString = fromMaybe "*" (show <$> total)
totalNotZero = fromMaybe True ((/=) 0 <$> total)
fromInRange = frm <= to
jsonH :: Header
jsonH = (hContentType, "application/json; charset=utf-8")
formatRelationError :: Text -> Text
formatRelationError = formatGeneralError
"could not find foreign keys between these entities"
formatParserError :: ParseError -> Text
formatParserError e = formatGeneralError message details
where
message = cs $ show (errorPos e)
details = strip $ replace "\n" " " $ cs
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
formatGeneralError :: Text -> Text -> Text
formatGeneralError message details = cs $ encode $ object [
"message" .= message,
"details" .= details]
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either Text ReadRequest
augumentRequestWithJoin schema allRels request =
(first formatRelationError . addRelations schema allRels Nothing) request
>>= addJoinConditions schema
buildReadRequest :: [Relation] -> ApiRequest -> Either Text ReadRequest
buildReadRequest allRels apiRequest =
augumentRequestWithJoin schema rels =<< first formatParserError (foldr addFilter <$> (addOrder <$> readRequest <*> ord) <*> flts)
where
selStr = iSelect apiRequest
orderS = iOrder apiRequest
action = iAction apiRequest
target = iTarget apiRequest
(schema, rootTableName) = fromJust $ -- Make it safe
case target of
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
_ -> Nothing
rootName = if action == ActionRead
then rootTableName
else sourceCTEName
filters = if action == ActionRead
then iFilters apiRequest
else filter (( '.' `elem` ) . fst) $ iFilters apiRequest -- there can be no filters on the root table whre we are doing insert/update
rels = case action of
ActionCreate -> fakeSourceRelations ++ allRels
ActionUpdate -> fakeSourceRelations ++ allRels
_ -> allRels
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels -- see comment in toSourceRelation
readRequest = parse (pRequestSelect rootName) ("failed to parse select parameter <<"++selStr++">>") selStr
addOrder (Node (q,i) f) o = Node (q{order=o}, i) f
flts = mapM pRequestFilter filters
ord = traverse (parse pOrder ("failed to parse order parameter <<"++fromMaybe "" orderS++">>")) orderS
buildMutateRequest :: ApiRequest -> Either Text MutateRequest
buildMutateRequest apiRequest =
mutateApiRequest
where
action = iAction apiRequest
target = iTarget apiRequest
payload = fromJust $ iPayload apiRequest
rootTableName = -- TODO: Make it safe
case target of
(TargetIdent (QualifiedIdentifier _ t) ) -> t
_ -> undefined
mutateApiRequest = case action of
ActionCreate -> Insert rootTableName <$> pure payload
ActionUpdate -> Update rootTableName <$> pure payload <*> cond
ActionDelete -> Delete rootTableName <$> cond
_ -> Left "Unsupported HTTP verb"
mutateFilters = filter (not . ( '.' `elem` ) . fst) $ iFilters apiRequest -- update/delete filters can be only on the root table
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 (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
Just tn -> Node rn (addFilter (remainingPath, flt) tn:restForest)
where
targetNodeName:remainingPath = path
(targetNode,restForest) = splitForest targetNodeName forest
splitForest name forst =
case maybeNode of
Nothing -> (Nothing,forest)
Just node -> (Just node, delete node forest)
where maybeNode = find ((name==).fst.snd.rootLabel) forst
-- in a relation where one of the tables mathces "TableName"
-- replace the name to that table with pg_source
-- this "fake" relations is needed so that in a mutate query
-- we can look a the "returning *" part which is wrapped with a "with"
-- as just another table that has relations with other tables
toSourceRelation :: TableName -> Relation -> Maybe Relation
toSourceRelation mt r@(Relation t _ ft _ _ rt _ _)
| mt == tableName t = Just $ r {relTable=t {tableName=sourceCTEName}}
| mt == tableName ft = Just $ r {relFTable=t {tableName=sourceCTEName}}
| Just mt == (tableName <$> rt) = Just $ r {relLTable=(\tbl -> tbl {tableName=sourceCTEName}) <$> rt}
| otherwise = Nothing
data TableOptions = TableOptions {
tblOptcolumns :: [Column]
, tblOptpkey :: [Text]
}
instance ToJSON TableOptions where
toJSON t = object [
"columns" .= tblOptcolumns t
, "pkey" .= tblOptpkey t ]
extractQueryResult :: Maybe ResultsWithCount -> ResultsWithCount
extractQueryResult = fromMaybe (Nothing, 0, "", "")
+87
View File
@@ -0,0 +1,87 @@
{-# LANGUAGE FlexibleContexts #-}
{-|
Module : PostgREST.Auth
Description : PostgREST authorization functions.
This module provides functions to deal with the JWT authorization (http://jwt.io).
It also can be used to define other authorization functions,
in the future Oauth, LDAP and similar integrations can be coded here.
Authentication should always be implemented in an external service.
In the test suite there is an example of simple login function that can be used for a
very simple authentication system inside the PostgreSQL database.
-}
module PostgREST.Auth (
setRole
, claimsToSQL
, jwtClaims
, tokenJWT
) where
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 (pgFmtIdent, pgFmtLit, unquoted)
import qualified Web.JWT as JWT
{-|
Receives a map of JWT claims and returns a list
of PostgreSQL statements to set the claims as user defined GUCs.
Except if we have a claim called role,
this one is mapped to a SET ROLE statement.
In case there is any problem decoding the JWT it returns Nothing.
-}
claimsToSQL :: M.HashMap Text Value -> [BS.ByteString]
claimsToSQL = map setVar . M.toList
where
setVar ("role", String val) = setRole val
setVar (k, val) = "set local " <> cs (pgFmtIdent $ "postgrest.claims." <> k)
<> " = " <> cs (valueToVariable val) <> ";"
valueToVariable = pgFmtLit . unquoted
{-|
Receives the JWT secret (from config) and a JWT and
returns a map of JWT claims
In case there is any problem decoding the JWT it returns Nothing.
-}
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) <> ";"
{-|
Receives the JWT secret (from config) and a JWT and a JSON value
and returns a signed JWT.
-}
tokenJWT :: JWT.Secret -> Value -> Text
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
+103
View File
@@ -0,0 +1,103 @@
{-|
Module : PostgREST.Config
Description : Manages PostgREST configuration options.
This module provides a helper function to read the command line arguments using the optparse-applicative
and the AppConfig type to store them.
It also can be used to define other middleware configuration that may be delegated to some sort of
external configuration.
It currently includes a hardcoded CORS policy but this could easly be turned in configurable behaviour if needed.
Other hardcoded options such as the minimum version number also belong here.
-}
module PostgREST.Config ( prettyVersion
, readOptions
, corsPolicy
, minimumPgVersion
, AppConfig (..)
)
where
import Control.Applicative
import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import Data.List (intercalate)
import Data.String.Conversions (cs)
import Data.Text (strip)
import Data.Version (versionBranch)
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)
-- | Data type to store all command line options
data AppConfig = AppConfig {
configDatabase :: String
, configAnonRole :: String
, configSchema :: String
, configPort :: Int
, configJwtSecret :: Secret
, configPool :: Int
, configMaxRows :: Maybe Integer
, configQuiet :: Bool
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> 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
["GET", "POST", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
(Just $ 60*60*24) False False True
-- | CORS policy to be used in by Wai Cors middleware
corsPolicy :: Request -> Maybe CorsResourcePolicy
corsPolicy req = case lookup "origin" headers of
Just origin -> Just defaultCorsPolicy {
corsOrigins = Just ([origin], True)
, corsRequestHeaders = "Authentication":accHeaders
, corsExposedHeaders = Just [
"Content-Encoding", "Content-Location", "Content-Range", "Content-Type"
, "Date", "Location", "Server", "Transfer-Encoding", "Range-Unit"
]
}
Nothing -> Nothing
where
headers = requestHeaders req
accHeaders = case lookup "access-control-request-headers" headers of
Just hdrs -> map (CI.mk . cs . strip . cs) $ BS.split ',' hdrs
Nothing -> []
-- | User friendly version number
prettyVersion :: String
prettyVersion = intercalate "." $ map show $ versionBranch version
-- | Function to read and parse options from the command line
readOptions :: IO AppConfig
readOptions = customExecParser parserPrefs opts
where
opts = info (helper <*> argParser) $
fullDesc
<> progDesc (
"PostgREST "
<> prettyVersion
<> " / create a REST API to an existing Postgres database"
)
parserPrefs = prefs showHelpOnError
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
minimumPgVersion :: Integer
minimumPgVersion = 90300
+637
View File
@@ -0,0 +1,637 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
module PostgREST.DbStructure (
getDbStructure
, accessibleTables
, doesProcExist
, doesProcReturnJWT
) where
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Query as H
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.Session DbStructure
getDbStructure schema = do
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
keys' = synonymousPrimaryKeys syns keys
return DbStructure {
dbTables = tabs
, dbColumns = cols'
, dbRelations = rels'
, dbPrimaryKeys = keys'
}
encodeQi :: HE.Params QualifiedIdentifier
encodeQi =
contramap qiSchema (HE.value HE.text) <>
contramap qiName (HE.value HE.text)
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 = $1
AND proname = $2
) |]
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 = $1
AND proname = $2
AND pg_catalog.pg_get_function_result(p.oid) like '%jwt_claims'
) |]
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'
where
syns = sort $ filter ((== colTable (head cols)) . colTable . fst) allSyns
synCols  = transpose $ map (\c -> map snd $ filter ((== c) . fst) syns) cols
synCols' = (filter sameTable . filter matchLength) synCols
matchLength cs = length cols == length cs
sameTable (c:cs) = all (\cc -> colTable c == colTable cc) (c:cs)
sameTable [] = False
addForeignKeys :: [Relation] -> [Column] -> [Column]
addForeignKeys rels = map addFk
where
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 _ _ = False
relToFk col Relation{relColumns=cols, relFColumns=colsF} = ForeignKey <$> colF
where
pos = elemIndex col cols
colF = (colsF !!) <$> pos
addSynonymousRelations :: [(Column,Column)] -> [Relation] -> [Relation]
addSynonymousRelations _ [] = []
addSynonymousRelations syns (rel:rels) = rel : synRelsP ++ synRelsF ++ addSynonymousRelations syns rels
where
synRelsP = synRels (relColumns rel) (\t cs -> rel{relTable=t,relColumns=cs})
synRelsF = synRels (relFColumns rel) (\t cs -> rel{relFTable=t,relFColumns=cs})
synRels cols mapFn = map (\cs -> mapFn (colTable $ head cs) cs) $ synonymousColumns syns cols
addParentRelations :: [Relation] -> [Relation]
addParentRelations [] = []
addParentRelations (rel@(Relation t c ft fc _ _ _ _):rels) = Relation ft fc t c Parent Nothing Nothing Nothing : rel : addParentRelations rels
addManyToManyRelations :: [Relation] -> [Relation]
addManyToManyRelations rels = rels ++ addMirrorRelation (mapMaybe link2Relation links)
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
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'
link2Relation [
Relation{relTable=lt, relColumns=lc1, relFTable=t, relFColumns=c},
Relation{ relColumns=lc2, relFTable=ft, relFColumns=fc}
]
| lc1 /= lc2 && length lc1 == 1 && length lc2 == 1 = Just $ Relation t c ft fc Many (Just lt) (Just lc1) (Just lc2)
| otherwise = Nothing
link2Relation _ = Nothing
raiseRelations :: Schema -> [(Column,Column)] -> [Relation] -> [Relation]
raiseRelations schema syns = map raiseRel
where
raiseRel rel
| tableSchema table == schema = rel
| isJust newCols = rel{relFTable=fromJust newTable,relFColumns=fromJust newCols}
| otherwise = rel
where
cols = relFColumns rel
table = relFTable rel
newCols = listToMaybe $ filter ((== schema) . tableSchema . colTable . head) (synonymousColumns syns cols)
newTable = (colTable . head) <$> newCols
synonymousPrimaryKeys :: [(Column,Column)] -> [PrimaryKey] -> [PrimaryKey]
synonymousPrimaryKeys _ [] = []
synonymousPrimaryKeys syns (key:keys) = key : newKeys ++ synonymousPrimaryKeys syns keys
where
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.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 |]
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,
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
where
buildColumn tbl = Column tbl n pos nul typ u l p d (parseEnum e) Nothing
table = find (\tbl -> tableSchema tbl == s && tableName tbl == t) tabs
parseEnum :: Maybe Text -> [Text]
parseEnum str = fromMaybe [] $ split (==',') <$> str
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,
ns2.nspname AS foreign_table_schema,
other.relname AS foreign_table_name,
column_info.refs AS foreign_columns
FROM pg_constraint,
LATERAL (SELECT array_agg(cols.attname) AS cols,
array_agg(cols.attnum) AS nums,
array_agg(refs.attname) AS refs
FROM ( SELECT unnest(conkey) AS col, unnest(confkey) AS ref) k,
LATERAL (SELECT * FROM pg_attribute
WHERE attrelid = conrelid AND attnum = col)
AS cols,
LATERAL (SELECT * FROM pg_attribute
WHERE attrelid = confrelid AND attnum = ref)
AS refs)
AS column_info,
LATERAL (SELECT * FROM pg_namespace WHERE pg_namespace.oid = connamespace) AS ns1,
LATERAL (SELECT * FROM pg_class WHERE pg_class.oid = conrelid) AS tab,
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) |]
relationFromRow :: [Table] -> [Column] -> (Text, Text, [Text], Text, Text, [Text]) -> Maybe Relation
relationFromRow allTabs allCols (rs, rt, rcs, frs, frt, frcs) =
Relation <$> table <*> cols <*> tableF <*> colsF <*> pure Child <*> pure Nothing <*> pure Nothing <*> pure Nothing
where
findTable s t = find (\tbl -> tableSchema tbl == s && tableName tbl == t) allTabs
findCol s t c = find (\col -> tableSchema (colTable col) == s && tableName (colTable col) == t && colName col == c) allCols
table = findTable rs rt
tableF = findTable frs frt
cols = mapM (findCol rs rt) rcs
colsF = mapM (findCol frs frt) frcs
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.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
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
|]
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe (Column,Column)
synonymFromRow allCols (s1,t1,c1,s2,t2,c2) = (,) <$> col1 <*> col2
where
col1 = findCol s1 t1 c1
col2 = findCol s2 t2 c2
findCol s t c = find (\col -> (tableSchema . colTable) col == s && (tableName . colTable) col == t && colName col == c) allCols
+94
View File
@@ -0,0 +1,94 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
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.Text (Text)
import qualified Data.Text as T
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)
errResponse :: HT.Status -> Text -> Response
errResponse status message = responseLBS status [(hContentType, "application/json")] (cs $ T.concat ["{\"message\":\"",message,"\"}"])
pgErrResponse :: P.UsageError -> Response
pgErrResponse e = responseLBS (httpStatus e)
[(hContentType, "application/json")] (JSON.encode e)
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.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)]
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
'3':'8':_ -> HT.status500 -- external routine exception
'3':'9':_ -> HT.status500 -- external routine invocation
'3':'B':_ -> HT.status500 -- savepoint exception
'4':'0':_ -> HT.status500 -- tx rollback
'5':'3':_ -> HT.status503 -- insufficient resources
'5':'4':_ -> HT.status413 -- too complex
'5':'5':_ -> HT.status500 -- obj not on prereq state
'5':'7':_ -> HT.status500 -- operator intervention
'5':'8':_ -> HT.status500 -- system error
'F':'0':_ -> HT.status500 -- conf file error
'H':'V':_ -> HT.status500 -- foreign data wrapper error
'P':'0':_ -> HT.status500 -- PL/pgSQL Error
'X':'X':_ -> HT.status500 -- internal Error
"42P01" -> HT.status404 -- undefined table
"42501" -> HT.status404 -- insufficient privilege
_ -> HT.status400
httpStatus (P.SessionError (H.ResultError _)) = HT.status500
httpStatus (P.SessionError (H.ClientError _)) = HT.status503
+78
View File
@@ -0,0 +1,78 @@
{-# LANGUAGE CPP #-}
module Main where
import PostgREST.App
import PostgREST.Config (AppConfig (..),
minimumPgVersion,
prettyVersion,
readOptions)
import PostgREST.DbStructure
import Control.Monad
import Data.Monoid ((<>))
import Data.String.Conversions (cs)
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 Bool
isServerVersionSupported = do
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
hSetBuffering stdout LineBuffering
hSetBuffering stdin LineBuffering
hSetBuffering stderr NoBuffering
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)
pool <- P.acquire (configPool conf, 10, pgSettings)
#ifndef mingw32_HOST_OS
tid <- myThreadId
forM_ [sigINT, sigTERM] $ \sig ->
void $ installHandler sig (Catch $ do
P.release pool
throwTo tid UserInterrupt
) Nothing
#endif
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)
let dbStructure = either (error.show) id result
runSettings appSettings $ postgrest conf dbStructure pool
+68
View File
@@ -0,0 +1,68 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
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.String.Conversions (cs)
import Data.Text
import Data.Time.Clock (NominalDiffTime)
import qualified Hasql.Transaction as H
import Network.HTTP.Types.Header (hAccept, hAuthorization)
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.Auth (setRole, jwtClaims, claimsToSQL)
import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (errResponse)
import Prelude hiding (concat, null)
runWithClaims :: AppConfig -> NominalDiffTime ->
(Request -> H.Transaction Response) ->
Request -> H.Transaction Response
runWithClaims conf time app req = do
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
hdrs = requestHeaders req
jwtSecret = configJwtSecret conf
auth = fromMaybe "" $ lookup hAuthorization hdrs
anon = cs $ configAnonRole conf
setAnon = setRole anon
clientErr = return . errResponse status400
unsupportedAccept :: Application -> Application
unsupportedAccept app req respond =
case accept of
Left _ -> respond $ errResponse status415 "Unsupported Accept header, try: application/json"
Right _ -> app req respond
where accept = pickContentType $ lookup hAccept $ requestHeaders req
defaultMiddle :: Application -> Application
defaultMiddle =
gzip def
. cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
. unsupportedAccept
+112
View File
@@ -0,0 +1,112 @@
module PostgREST.Parsers
-- ( parseGetRequest
-- )
where
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, (<|>))
pRequestSelect :: Text -> Parser ReadRequest
pRequestSelect rootNodeName = do
fieldTree <- pFieldForest
return $ foldr treeEntry (Node (Select [] [rootNodeName] [] Nothing, (rootNodeName, Nothing)) []) fieldTree
where
treeEntry :: Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry (Node fld@((fn, _),_) fldForest) (Node (q, i) rForest) =
case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) (foldr treeEntry (Node (Select [] [fn] [] Nothing, (fn, Nothing)) []) fldForest:rForest)
pRequestFilter :: (String, String) -> Either ParseError (Path, Filter)
pRequestFilter (k, v) = (,) <$> path <*> (Filter <$> fld <*> op <*> val)
where
treePath = parse pTreePath ("failed to parser tree path (" ++ k ++ ")") k
opVal = parse pOpValueExp ("failed to parse filter (" ++ v ++ ")") v
path = fst <$> treePath
fld = snd <$> treePath
op = fst <$> opVal
val = snd <$> opVal
ws :: Parser Text
ws = cs <$> many (oneOf " \t")
lexeme :: Parser a -> Parser a
lexeme p = ws *> p <* ws
pTreePath :: Parser (Path,Field)
pTreePath = do
p <- pFieldName `sepBy1` pDelimiter
jp <- optionMaybe pJsonPath
let pp = map cs p
jpp = map cs <$> jp
return (init pp, (last pp, jpp))
pFieldForest :: Parser [Tree SelectItem]
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
pFieldTree :: Parser (Tree SelectItem)
pFieldTree = try (Node <$> pSelect <*> between (char '{') (char '}') pFieldForest)
<|> Node <$> pSelect <*> pure []
pStar :: Parser Text
pStar = cs <$> (string "*" *> pure ("*"::String))
pFieldName :: Parser Text
pFieldName = cs <$> (many1 (letter <|> digit <|> oneOf "_")
<?> "field name (* or [a..z0..9_])")
pJsonPathStep :: Parser Text
pJsonPathStep = cs <$> try (string "->" *> pFieldName)
pJsonPath :: Parser [Text]
pJsonPath = (++) <$> many pJsonPathStep <*> ( (:[]) <$> (string "->>" *> pFieldName) )
pField :: Parser Field
pField = lexeme $ (,) <$> pFieldName <*> optionMaybe pJsonPath
pSelect :: Parser SelectItem
pSelect = lexeme $
try ((,) <$> pField <*>((cs <$>) <$> optionMaybe (string "::" *> many letter)) )
<|> do
s <- pStar
return ((s, Nothing), Nothing)
pOperator :: Parser Operator
pOperator = cs <$> (pOp <?> "operator (eq, gt, ...)")
where pOp = foldl (<|>) empty $ map (try . string . cs . fst) operators
pValue :: Parser FValue
pValue = VText <$> (cs <$> many anyChar)
pDelimiter :: Parser Char
pDelimiter = char '.' <?> "delimiter (.)"
pOperatiorWithNegation :: Parser Operator
pOperatiorWithNegation = try ( (<>) <$> ( cs <$> string "not." ) <*> pOperator) <|> pOperator
pOpValueExp :: Parser (Operator, FValue)
pOpValueExp = (,) <$> pOperatiorWithNegation <*> (pDelimiter *> pValue)
pOrder :: Parser [OrderTerm]
pOrder = lexeme pOrderTerm `sepBy` char ','
pOrderTerm :: Parser OrderTerm
pOrderTerm =
try ( do
c <- pFieldName
_ <- pDelimiter
d <- (string "asc" *> pure OrderAsc)
<|> (string "desc" *> pure OrderDesc)
nls <- optionMaybe (pDelimiter *> (
try(string "nullslast" *> pure OrderNullsLast)
<|> try(string "nullsfirst" *> pure OrderNullsFirst)
))
return $ OrderTerm c d nls
)
<|> OrderTerm <$> (cs <$> pFieldName) <*> pure OrderAsc <*> pure Nothing
+519
View File
@@ -0,0 +1,519 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE TupleSections #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-|
Module : PostgREST.QueryBuilder
Description : PostgREST SQL generating functions.
This module provides functions to consume data types that
represent database objects (e.g. Relation, Schema, SqlQuery)
and produces SQL Statements.
Any function that outputs a SQL fragment should be in this module.
-}
module PostgREST.QueryBuilder (
addRelations
, addJoinConditions
, callProc
, createReadStatement
, createWriteStatement
, operators
, pgFmtIdent
, pgFmtLit
, requestToQuery
, requestToCountQuery
, sourceCTEName
, unquoted
, ResultsWithCount
) where
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 ((<|>))
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 (..)
, formatScientific
, isInteger
)
import Prelude hiding (unwords)
import PostgREST.ApiRequest (PreferRepresentation (..))
{-| 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 =
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",
"'' AS header",
bodyF <> " AS body"
]
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| otherwise = asJsonF
createWriteStatement :: QualifiedIdentifier -> SqlQuery -> SqlQuery -> Bool ->
PreferRepresentation -> [Text] -> Bool -> Payload ->
H.Query UniformObjects (Maybe ResultsWithCount)
createWriteStatement _ _ _ _ _ _ _ (PayloadParseError _) = undefined
createWriteStatement _ _ mutateQuery _ None
_ _ (PayloadJSON (UniformObjects _)) =
unicodeStatement sql encodeUniformObjs decodeStandardMay True
where
sql = [qc|
WITH {sourceCTEName} AS ({mutateQuery})
SELECT '', 0, '', '' |]
createWriteStatement qi _ mutateQuery isSingle HeadersOnly
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 "''",
"''"
]
createWriteStatement qi selectQuery mutateQuery isSingle Full
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 "''" <> " AS header",
bodyF <> " AS body"
]
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) =
case parentNode of
(Just (Node (Select{from=[parentTable]}, (_, _)) _)) -> Node <$> (addRel readNode <$> rel) <*> updatedForest
where
rel = note ("no relation between " <> parentTable <> " and " <> name)
$ findRelationByTable schema name parentTable
<|> findRelationByColumn schema parentTable name
addRel :: (ReadQuery, (NodeName, Maybe Relation)) -> Relation -> (ReadQuery, (NodeName, Maybe Relation))
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
updatedForest = mapM (addRelations schema allRelations (Just node)) forest
-- Searches through all the relations and returns a match given the parameter conditions.
-- Will only find a relation where both schemas are in the PostgREST schema.
-- `findRelationByColumn` also does a ducktype check to see if the column name has any variation of `id` or `fk`. If so then the relation is returned as a match.
findRelationByTable s t1 t2 =
find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t1 == tableName (relTable r) && t2 == tableName (relFTable r)) allRelations
findRelationByColumn s t c =
find (\r -> s == tableSchema (relTable r) && s == tableSchema (relFTable r) && t == tableName (relFTable r) && length (relFColumns r) == 1 && c `colMatches` (colName . head . relFColumns) r) allRelations
where n `colMatches` rc = (cs ("^" <> rc <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (cs n :: BS.ByteString)
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)} ->
Node (qq, (n, r)) <$> updatedForest
where
query' = addCond updatedQuery (getJoinConditions rel)
qq = query'{from=tableName linkTable : from query'}
_ -> Left "unknown relation"
where
-- add parentTable and parentJoinConditions to the query
updatedQuery = foldr (flip addCond) query parentJoinConditions
where
parentJoinConditions = map (getJoinConditions . snd) parents
parents = mapMaybe (getParents . rootLabel) forest
getParents (_, (tbl, Just rel@Relation{relType=Parent})) = Just (tbl, rel)
getParents _ = Nothing
updatedForest = mapM (addJoinConditions schema) forest
addCond query' con = query'{flt_=con ++ flt_ query'}
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
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 = [
("eq", "="),
("gte", ">="), -- has to be before gt (parsers)
("gt", ">"),
("lte", "<="), -- has to be before lt (parsers)
("lt", "<"),
("neq", "<>"),
("like", "like"),
("ilike", "ilike"),
("in", "in"),
("notin", "not in"),
("isnot", "is not"), -- has to be before is (parsers)
("is", "is"),
("@@", "@@"),
("@>", "@>"),
("<@", "<@")
]
pgFmtIdent :: SqlFragment -> SqlFragment
pgFmtIdent x = "\"" <> replace "\"" "\"\"" (trimNullChars $ cs x) <> "\""
pgFmtLit :: SqlFragment -> SqlFragment
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> replace "'" "''" trimmed <> "'"
slashed = replace "\\" "\\\\" escaped in
if "\\" `isInfixOf` escaped
then "E" <> slashed
else slashed
requestToCountQuery :: Schema -> DbRequest -> SqlQuery
requestToCountQuery _ (DbMutate _) = undefined
requestToCountQuery schema (DbRead (Node (Select _ _ conditions _, (mainTbl, _)) _)) =
unwords [
"SELECT pg_catalog.count(1)",
"FROM ", fromQi $ QualifiedIdentifier schema mainTbl,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition (QualifiedIdentifier schema mainTbl)) localConditions )) `emptyOnNull` localConditions
]
where
fn Filter{value=VText _} = True
fn Filter{value=VForeignKey _ _} = False
localConditions = filter fn conditions
requestToQuery :: Schema -> DbRequest -> SqlQuery
requestToQuery _ (DbMutate (Insert _ (PayloadParseError _))) = undefined
requestToQuery _ (DbMutate (Update _ (PayloadParseError _) _)) = undefined
requestToQuery schema (DbRead (Node (Select colSelects tbls conditions ord, (nodeName, maybeRelation)) forest)) =
query
where
-- TODO! the folloing helper functions are just to remove the "schema" part when the table is "source" which is the name
-- of our WITH query part
mainTbl = fromMaybe nodeName (tableName . relTable <$> maybeRelation)
tblSchema tbl = if tbl == sourceCTEName then "" else schema
qi = QualifiedIdentifier (tblSchema mainTbl) mainTbl
toQi t = QualifiedIdentifier (tblSchema t) t
query = unwords [
"SELECT ", intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
"FROM ", intercalate ", " (map (fromQi . toQi) tbls),
unwords (map joinStr joins),
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) localConditions )) `emptyOnNull` localConditions,
orderF (fromMaybe [] ord)
]
orderF ts =
if null ts
then ""
else "ORDER BY " <> clause
where
clause = intercalate "," (map queryTerm ts)
queryTerm :: OrderTerm -> Text
queryTerm t = " "
<> cs (pgFmtColumn qi $ otTerm t) <> " "
<> (cs.show) (otDirection t) <> " "
<> maybe "" (cs.show) (otNullOrder t) <> " "
(joins, selects) = foldr getQueryParts ([],[]) forest
parentTables = map snd joins
parentConditions = join $ map (( `filter` conditions ) . filterParentConditions) parentTables
localConditions = conditions \\ parentConditions
joinStr :: (SqlFragment, TableName) -> SqlFragment
joinStr (sql, t) = "LEFT OUTER JOIN " <> sql <> " ON " <>
intercalate " AND " ( map (pgFmtCondition qi ) joinConditions )
where
joinConditions = filter (filterParentConditions t) conditions
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)
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)
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)
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))
--the following is just to remove the warning
--getQueryParts is not total but requestToQuery is called only after addJoinConditions which ensures the only
--posible relations are Child Parent Many
getQueryParts (Node (_,(_,Nothing)) _) _ = undefined
requestToQuery schema (DbMutate (Insert mainTbl (PayloadJSON (UniformObjects rows)))) =
let qi = QualifiedIdentifier schema mainTbl
cols = map pgFmtIdent $ fromMaybe [] (HM.keys <$> (rows V.!? 0))
colsString = intercalate ", " cols in
unwords [
"INSERT INTO ", fromQi qi,
" (" <> colsString <> ")" <>
" SELECT " <> colsString <>
" FROM json_populate_recordset(null::" , fromQi qi, ", $1)"
]
requestToQuery schema (DbMutate (Update mainTbl (PayloadJSON (UniformObjects rows)) conditions)) =
case rows V.!? 0 of
Just obj ->
let assignments = map
(\(k,v) -> pgFmtIdent k <> "=" <> insertableValue v) $ HM.toList obj in
unwords [
"UPDATE ", fromQi qi,
" SET " <> intercalate "," assignments <> " ",
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions
]
Nothing -> undefined
where
qi = QualifiedIdentifier schema mainTbl
requestToQuery schema (DbMutate (Delete mainTbl conditions)) =
query
where
qi = QualifiedIdentifier schema mainTbl
query = unwords [
"DELETE FROM ", fromQi qi,
("WHERE " <> intercalate " AND " ( map (pgFmtCondition qi ) conditions )) `emptyOnNull` conditions
]
sourceCTEName :: SqlFragment
sourceCTEName = "pg_source"
unquoted :: JSON.Value -> Text
unquoted (JSON.String t) = t
unquoted (JSON.Number n) =
cs $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
unquoted (JSON.Bool b) = cs . show $ b
unquoted v = cs $ JSON.encode v
-- private functions
asCsvF :: SqlFragment
asCsvF = asCsvHeaderF <> " || '\n' || " <> asCsvBodyF
where
asCsvHeaderF =
"(SELECT coalesce(string_agg(a.k, ','), '')" <>
" FROM (" <>
" SELECT json_object_keys(r)::TEXT as k" <>
" FROM ( " <>
" SELECT row_to_json(hh) as r from " <> sourceCTEName <> " as hh limit 1" <>
" ) s" <>
" ) a" <>
")"
asCsvBodyF = "coalesce(string_agg(substring(t::text, 2, length(t::text) - 2), '\n'), '')"
asJsonF :: SqlFragment
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 = "coalesce(string_agg(row_to_json(t)::text, ','), '')::character varying "
locationF :: [Text] -> SqlFragment
locationF pKeys =
"(" <>
" WITH s AS (SELECT row_to_json(ss) as r from " <> sourceCTEName <> " as ss limit 1)" <>
" SELECT string_agg(json_data.key || '=' || coalesce( 'eq.' || json_data.value, 'is.null'), '&')" <>
" FROM s, json_each_text(s.r) AS json_data" <>
(
if null pKeys
then ""
else " WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')"
) <> ")"
limitF :: NonnegRange -> SqlFragment
limitF r = "LIMIT " <> limit <> " OFFSET " <> offset
where
limit = maybe "ALL" (cs . show) $ rangeLimit r
offset = cs . show $ rangeOffset r
fromQi :: QualifiedIdentifier -> SqlFragment
fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n
where
n = qiName t
s = qiSchema t
getJoinConditions :: Relation -> [Filter]
getJoinConditions (Relation t cols ft fcs typ lt lc1 lc2) =
case typ of
Child -> zipWith (toFilter tN ftN) cols fcs
Parent -> zipWith (toFilter tN ftN) cols fcs
Many -> zipWith (toFilter tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toFilter ftN ltN) fcs (fromMaybe [] lc2)
where
s = if typ == Parent then "" else tableSchema t
tN = tableName t
ftN = tableName ft
ltN = fromMaybe "" (tableName <$> lt)
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
insertableValue :: JSON.Value -> SqlFragment
insertableValue JSON.Null = "null"
insertableValue v = (<> "::unknown") . pgFmtLit $ unquoted v
whiteList :: Text -> SqlFragment
whiteList val = fromMaybe
(cs (pgFmtLit val) <> "::unknown ")
(find ((==) . toLower $ val) ["null","true","false"])
pgFmtColumn :: QualifiedIdentifier -> Text -> SqlFragment
pgFmtColumn table "*" = fromQi table <> ".*"
pgFmtColumn table c = fromQi table <> "." <> pgFmtIdent c
pgFmtField :: QualifiedIdentifier -> Field -> SqlFragment
pgFmtField table (c, jp) = pgFmtColumn table c <> pgFmtJsonPath jp
pgFmtSelectItem :: QualifiedIdentifier -> SelectItem -> SqlFragment
pgFmtSelectItem table (f@(_, jp), Nothing) = pgFmtField table f <> pgFmtAsJsonPath jp
pgFmtSelectItem table (f@(_, jp), Just cast ) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAsJsonPath jp
pgFmtCondition :: QualifiedIdentifier -> Filter -> SqlFragment
pgFmtCondition table (Filter (col,jp) ops val) =
notOp <> " " <> sqlCol <> " " <> pgFmtOperator opCode <> " " <>
if opCode `elem` ["is","isnot"] then whiteList (getInner val) else sqlValue
where
headPredicate:rest = split (=='.') ops
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
opCode = hasNot (head rest) headPredicate
notOp = hasNot headPredicate ""
sqlCol = case val of
VText _ -> pgFmtColumn table col <> pgFmtJsonPath jp
VForeignKey qi _ -> pgFmtColumn qi col
sqlValue = valToStr val
getInner v = case v of
VText s -> s
_ -> ""
valToStr v = case v of
VText s -> pgFmtValue opCode s
VForeignKey (QualifiedIdentifier s _) (ForeignKey Column{colTable=Table{tableName=ft}, colName=fc}) -> pgFmtColumn qi fc
where qi = QualifiedIdentifier (if ft == sourceCTEName then "" else s) ft
_ -> ""
pgFmtValue :: Text -> Text -> SqlFragment
pgFmtValue opCode val =
case opCode of
"like" -> unknownLiteral $ T.map star val
"ilike" -> unknownLiteral $ T.map star val
"in" -> "(" <> intercalate ", " (map unknownLiteral $ split (==',') val) <> ") "
"notin" -> "(" <> intercalate ", " (map unknownLiteral $ split (==',') val) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral val <> ") "
_ -> unknownLiteral val
where
star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit
pgFmtOperator :: Text -> SqlFragment
pgFmtOperator opCode = fromMaybe "=" $ M.lookup opCode operatorsMap
where
operatorsMap = M.fromList operators
pgFmtJsonPath :: Maybe JsonPath -> SqlFragment
pgFmtJsonPath (Just [x]) = "->>" <> pgFmtLit x
pgFmtJsonPath (Just (x:xs)) = "->" <> pgFmtLit x <> pgFmtJsonPath ( Just xs )
pgFmtJsonPath _ = ""
pgFmtAsJsonPath :: Maybe JsonPath -> SqlFragment
pgFmtAsJsonPath Nothing = ""
pgFmtAsJsonPath (Just xx) = " AS " <> last xx
trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0')
+68
View File
@@ -0,0 +1,68 @@
module PostgREST.RangeQuery (
rangeParse
, rangeRequested
, rangeLimit
, rangeOffset
, restrictRange
, NonnegRange
) where
import Control.Applicative
import Network.HTTP.Types.Header
import PostgREST.Types ()
import qualified Data.ByteString.Char8 as BS
import Data.Ranged.Boundaries
import Data.Ranged.Ranges
import Data.String.Conversions (cs)
import Text.Read (readMaybe)
import Text.Regex.TDFA ((=~))
import Data.Maybe (fromMaybe, listToMaybe)
import Prelude
type NonnegRange = Range Integer
rangeParse :: BS.ByteString -> NonnegRange
rangeParse range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
Just parsedRange ->
let [_, from, to] = readMaybe . cs <$> parsedRange
lower = fromMaybe emptyRange (rangeGeq <$> from)
upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to) in
rangeIntersection lower upper
Nothing -> rangeGeq 0
rangeRequested :: RequestHeaders -> NonnegRange
rangeRequested = rangeParse . fromMaybe "" . lookup hRange
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 Integer
rangeLimit range =
case [rangeLower range, rangeUpper range] of
[BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
rangeOffset :: NonnegRange -> Integer
rangeOffset range =
case rangeLower range of
BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
rangeGeq :: Integer -> NonnegRange
rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll
rangeLeq :: Integer -> NonnegRange
rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n)
+160
View File
@@ -0,0 +1,160 @@
module PostgREST.Types where
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
data DbStructure = DbStructure {
dbTables :: [Table]
, dbColumns :: [Column]
, dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey]
} deriving (Show, Eq)
type Schema = Text
type TableName = Text
type SqlQuery = Text
type SqlFragment = Text
type RequestBody = BL.ByteString
data Table = Table {
tableSchema :: Schema
, tableName :: TableName
, tableInsertable :: Bool
} deriving (Show, Ord)
data ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord)
data Column =
Column {
colTable :: Table
, colName :: Text
, colPosition :: Int32
, colNullable :: Bool
, colType :: Text
, colUpdatable :: Bool
, colMaxLen :: Maybe Int32
, colPrecision :: Maybe Int32
, colDefault :: Maybe Text
, colEnum :: [Text]
, colFK :: Maybe ForeignKey
}
| Star { colTable :: Table }
deriving (Show, Ord)
type Synonym = (Column,Column)
data PrimaryKey = PrimaryKey {
pkTable :: Table
, pkName :: Text
} deriving (Show, Eq)
data OrderDirection = OrderAsc | OrderDesc deriving (Eq)
instance Show OrderDirection where
show OrderAsc = "asc"
show OrderDesc = "desc"
data OrderNulls = OrderNullsFirst | OrderNullsLast deriving (Eq)
instance Show OrderNulls where
show OrderNullsFirst = "nulls first"
show OrderNullsLast = "nulls last"
data OrderTerm = OrderTerm {
otTerm :: Text
, otDirection :: OrderDirection
, otNullOrder :: Maybe OrderNulls
} deriving (Show, Eq)
data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: Schema
, qiName :: TableName
} deriving (Show, Eq)
data RelationType = Child | Parent | Many deriving (Show, Eq)
data Relation = Relation {
relTable :: Table
, relColumns :: [Column]
, relFTable :: Table
, relFColumns :: [Column]
, relType :: RelationType
, relLTable :: Maybe Table
, relLCols1 :: Maybe [Column]
, relLCols2 :: Maybe [Column]
} deriving (Show, Eq)
-- | An array of JSON objects that has been verified to have
-- the same keys in every object
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.
data Payload = PayloadJSON UniformObjects
| PayloadParseError BS.ByteString
deriving (Show, Eq)
type Operator = Text
data FValue = VText Text | VForeignKey QualifiedIdentifier ForeignKey deriving (Show, Eq)
type FieldName = Text
type JsonPath = [Text]
type Field = (FieldName, Maybe JsonPath)
type Cast = Text
type NodeName = Text
type SelectItem = (Field, Maybe Cast)
type Path = [Text]
data ReadQuery = Select { select::[SelectItem], from::[TableName], flt_::[Filter], order::Maybe [OrderTerm] } deriving (Show, Eq)
data MutateQuery = Insert { in_::TableName, qPayload::Payload }
| Delete { in_::TableName, where_::[Filter] }
| Update { in_::TableName, qPayload::Payload, where_::[Filter] } deriving (Show, Eq)
data Filter = Filter {field::Field, operator::Operator, value::FValue} deriving (Show, Eq)
type ReadNode = (ReadQuery, (NodeName, Maybe Relation))
type ReadRequest = Tree ReadNode
type MutateRequest = MutateQuery
data DbRequest = DbRead ReadRequest | DbMutate MutateRequest
instance ToJSON Column where
toJSON c = object [
"schema" .= tableSchema t
, "name" .= colName c
, "position" .= colPosition c
, "nullable" .= colNullable c
, "type" .= colType c
, "updatable" .= colUpdatable c
, "maxLen" .= colMaxLen c
, "precision" .= colPrecision c
, "references".= colFK c
, "default" .= colDefault c
, "enum" .= colEnum c ]
where
t = colTable c
instance ToJSON ForeignKey where
toJSON fk = object [
"schema" .= tableSchema t
, "table" .= tableName t
, "column" .= colName c ]
where
c = fkCol fk
t = colTable c
instance ToJSON Table where
toJSON v = object [
"schema" .= tableSchema v
, "name" .= tableName v
, "insertable" .= tableInsertable v ]
instance Eq Table where
Table{tableSchema=s1,tableName=n1} == Table{tableSchema=s2,tableName=n2} = s1 == s2 && n1 == n2
instance Eq Column where
Column{colTable=t1,colName=n1} == Column{colTable=t2,colName=n2} = t1 == t2 && n1 == n2
_ == _ = False
-58
View File
@@ -1,58 +0,0 @@
module RangeQuery (
rangeParse
, rangeRequested
, rangeLimit
, rangeOffset
, NonnegRange
) where
import Control.Applicative
import Network.HTTP.Types.Header
import qualified Data.ByteString.Char8 as BS
import Data.Ranged.Boundaries
import Data.Ranged.Ranges
import Data.String.Conversions (cs)
import Text.Regex.TDFA ((=~))
import Text.Read (readMaybe)
import Data.Maybe (fromMaybe, listToMaybe)
type NonnegRange = Range Int
rangeParse :: BS.ByteString -> Maybe NonnegRange
rangeParse range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
parsedRange <- listToMaybe (range =~ rangeRegex :: [[BS.ByteString]])
let [_, from, to] = readMaybe . cs <$> parsedRange
let lower = fromMaybe emptyRange (rangeGeq <$> from)
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
return $ rangeIntersection lower upper
rangeRequested :: RequestHeaders -> Maybe NonnegRange
rangeRequested = (rangeParse =<<) . lookup hRange
rangeLimit :: NonnegRange -> Maybe Int
rangeLimit range =
case [rangeLower range, rangeUpper range]
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
rangeOffset :: NonnegRange -> Int
rangeOffset range =
case rangeLower range
of BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
rangeGeq :: Int -> NonnegRange
rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll
rangeLeq :: Int -> NonnegRange
rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n)
-57
View File
@@ -1,57 +0,0 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
module Types where
import qualified Data.Aeson as JSON
import Data.Aeson.Types (Parser)
import Data.Scientific (floatingOrInteger)
import Data.HashMap.Strict (foldlWithKey')
import Data.Text (Text)
import Data.Text.Encoding (decodeUtf8)
import Data.Time.Calendar (showGregorian)
import Control.Monad (mzero)
instance JSON.FromJSON SqlValue where
parseJSON (JSON.Number n) = return $ either toSql iToSql (floatingOrInteger n :: Either Double Int)
parseJSON (JSON.String s) = return $ toSql s
parseJSON (JSON.Bool b) = return $ toSql b
parseJSON JSON.Null = return SqlNull
parseJSON (JSON.Object o) = return . toSql $ JSON.encode o
parseJSON (JSON.Array a) = return . toSql $ JSON.encode a
instance JSON.ToJSON SqlValue where
toJSON (SqlString s) = JSON.toJSON s
toJSON (SqlByteString s) = JSON.toJSON $ decodeUtf8 s
toJSON (SqlWord32 w) = JSON.toJSON w
toJSON (SqlWord64 w) = JSON.toJSON w
toJSON (SqlInt32 i) = JSON.toJSON i
toJSON (SqlInt64 i) = JSON.toJSON i
toJSON (SqlInteger i) = JSON.toJSON i
toJSON (SqlChar c) = JSON.toJSON c
toJSON (SqlBool b) = JSON.toJSON b
toJSON (SqlDouble n) = JSON.toJSON n
toJSON (SqlRational n) = JSON.toJSON n
toJSON (SqlLocalDate d) = JSON.toJSON $ showGregorian d
toJSON (SqlLocalTimeOfDay t) = JSON.toJSON $ show t
toJSON (SqlLocalTime t) = JSON.toJSON $ show t
toJSON SqlNull = JSON.Null
toJSON x = JSON.toJSON $ show x
newtype SqlRow = SqlRow {getRow :: [(Text, SqlValue)] } deriving (Show)
sqlRowColumns :: SqlRow -> [Text]
sqlRowColumns = map fst . getRow
sqlRowValues :: SqlRow -> [SqlValue]
sqlRowValues = map snd . getRow
instance JSON.FromJSON SqlRow where
parseJSON (JSON.Object m) = foldlWithKey' add (return $ SqlRow []) m
where
add :: Parser SqlRow -> Text -> JSON.Value -> Parser SqlRow
add parser k v = do
SqlRow l <- parser
sqlV <- JSON.parseJSON v
return . SqlRow $ (k, sqlV) : l
parseJSON _ = mzero
+15
View File
@@ -0,0 +1,15 @@
resolver: lts-5.5
extra-deps:
- Ranged-sets-0.3.0
- 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:
- '.'
+65 -11
View File
@@ -7,24 +7,78 @@ import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
-- }}}
spec :: Spec
spec = beforeAll
(clearTable "postgrest.auth") . afterAll_ (clearTable "postgrest.auth")
$ around withApp
$ describe "authorization" $ do
spec :: SpecWith Application
spec = describe "authorization" $ do
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
it "indicates login failure" $ do
let auth = authHeader "postgrest_test_author" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "returns jwt functions as jwt tokens" $
post "/rpc/login" [json| { "id": "jdoe", "pass": "1234" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
, matchStatus = 200
, 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
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeader "jdoe" "1234"
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "works with tokens which have extra fields" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.GfydCh-F4wnM379xs0n1zUgalwJIsb6YoBapCo8HlFk"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
-- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with an unexpired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.QaPPLWTuyydMu_q7H4noMT7Lk6P4muet1OpJXF6ofhc"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.enk_qZ_u6gZsXY4R8bREKB_HNExRpM0lIWSLktk9JJQ"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "hides tables from users with invalid JWT" $ do
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "should fail when jwt contains no claims" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.MKYc_lOECtB0LJOiykilAdlHodB-I0_id2qHKq35dmc"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "hides tables from users with JWT that contain no claims about role" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.zyohGMnrDy4_8eJTl6I2AUXO3MeCCiwR24aGWRkTE9o"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 400
it "recovers after 400 error with logged in user" $ do
_ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
_ <- request methodPost "/rpc/problem" [auth] ""
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+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
+6 -4
View File
@@ -9,10 +9,12 @@ import qualified Data.ByteString.Lazy as BL
import SpecHelper
import Network.HTTP.Types
import Network.Wai (Application)
-- }}}
spec :: Spec
spec = around withApp $ describe "CORS" $ do
spec :: SpecWith Application
spec =
describe "CORS" $ do
let preflightHeaders = [
("Accept", "*/*"),
("Origin", "http://example.com"),
@@ -22,7 +24,7 @@ spec = around withApp $ describe "CORS" $ do
("Host", "localhost:3000"),
("User-Agent", "Mozilla/5.0 (Macintosh; Intel Mac OS X 10.9; rv:32.0) Gecko/20100101 Firefox/32.0"),
("Origin", "http://localhost:8000"),
("Accept", "text/plain, */*; q=0.01"),
("Accept", "text/csv, */*; q=0.01"),
("Accept-Language", "en-US,en;q=0.5"),
("Accept-Encoding", "gzip, deflate"),
("Referer", "http://localhost:8000/"),
@@ -41,7 +43,7 @@ spec = around withApp $ describe "CORS" $ do
"true"
respHeaders `shouldSatisfy` matchHeader
"Access-Control-Allow-Methods"
"GET, POST, PUT, PATCH, DELETE, OPTIONS, HEAD"
"GET, POST, PATCH, DELETE, OPTIONS, HEAD"
respHeaders `shouldSatisfy` matchHeader
"Access-Control-Allow-Headers"
"Authentication, Foo, Bar, Accept, Accept-Language, Content-Language"
+5 -5
View File
@@ -2,13 +2,13 @@ module Feature.DeleteSpec where
import Test.Hspec
import Test.Hspec.Wai
import SpecHelper
import Text.Heredoc
import Network.HTTP.Types
import Network.Wai (Application)
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
spec :: SpecWith Application
spec =
describe "Deleting" $ do
context "existing record" $ do
it "succeeds with 204 and deletion count" $
@@ -23,7 +23,7 @@ spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable
_ <- request methodDelete "/items?id=lt.15" [] ""
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":15}]"
matchBody = Just [str|[{"id":15}]|]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
+252 -36
View File
@@ -1,6 +1,6 @@
module Feature.InsertSpec where
import Test.Hspec
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
@@ -9,28 +9,53 @@ import SpecHelper
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 :: Spec
spec = afterAll_ resetDb $ around withApp $ do
spec :: SpecWith Application
spec = do
describe "Posting new record" $ do
after_ (clearTable "menagerie") . it "accepts disparate json types" $ do
p <- post "/menagerie"
[json| {
"integer": 13, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
context "disparate json types" $ do
it "accepts disparate json types" $ do
p <- post "/menagerie"
[json| {
"integer": 13, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
it "filters columns in result using &select" $
request methodPost "/menagerie?select=integer,varchar" [("Prefer", "return=representation")]
[json| {
"integer": 14, "double": 3.14159, "varchar": "testing!"
, "boolean": false, "date": "1900-01-01", "money": "$3.99"
, "enum": "foo"
} |] `shouldRespondWith` ResponseMatcher {
matchBody = Just [str|{"integer":14,"varchar":"testing!"}|]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json; charset=utf-8"]
}
it "includes related data after insert" $
request methodPost "/projects?select=id,name,clients{id,name}" [("Prefer", "return=representation")]
[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; charset=utf-8", "Location" <:> "/projects?id=eq.6"]
}
context "with no pk supplied" $ do
context "into a table with auto-incrementing pk" . after_ (clearTable "auto_incrementing_pk") $
context "into a table with auto-incrementing pk" $
it "succeeds with 201 and link" $ do
p <- post "/auto_incrementing_pk" [json| { "non_nullable_string":"not null"} |]
liftIO $ do
@@ -49,7 +74,7 @@ spec = afterAll_ resetDb $ around withApp $ do
post "/simple_pk" [json| { "extra":"foo"} |]
`shouldRespondWith` 400
context "into a table with no pk" . after_ (clearTable "no_pk") $
context "into a table with no pk" $ do
it "succeeds with 201 and a link including all fields" $ do
p <- post "/no_pk" [json| { "a":"foo", "b":"bar" } |]
liftIO $ do
@@ -57,7 +82,34 @@ spec = afterAll_ resetDb $ around withApp $ do
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.foo&b=eq.bar"
simpleStatus p `shouldBe` created201
context "with compound pk supplied" . after_ (clearTable "compound_pk") $
it "returns full details of inserted record if asked" $ do
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
[json| { "a":"bar", "b":"baz" } |]
liftIO $ do
simpleBody p `shouldBe` [json| { "a":"bar", "b":"baz" } |]
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=eq.bar&b=eq.baz"
simpleStatus p `shouldBe` created201
it "can insert in tables with no select privileges" $ do
p <- request methodPost "/insertonly"
[("Prefer", "return=minimal")]
[json| { "v":"some value" } |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
it "can post nulls" $ do
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
[json| { "a":null, "b":"foo" } |]
liftIO $ do
simpleBody p `shouldBe` [json| { "a":null, "b":"foo" } |]
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/no_pk\\?a=is.null&b=eq.foo"
simpleStatus p `shouldBe` created201
context "with compound pk supplied" $
it "builds response location header appropriately" $
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
`shouldRespondWith` ResponseMatcher {
@@ -68,24 +120,117 @@ spec = afterAll_ resetDb $ around withApp $ do
context "with invalid json payload" $
it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2"
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" } } |]
request methodPost "/json"
[("Prefer", "return=representation")]
inserted
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"message":"Failed to parse JSON payload. Failed reading: satisfy"} |]
, matchStatus = 400
, matchHeaders = []
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Location" <:> [str|/json?data=eq.{"foo":"bar"}|]]
}
it "serializes nested array" $ do
let inserted = [json| { "data": [1,2,3] } |]
request methodPost "/json"
[("Prefer", "return=representation")]
inserted
`shouldRespondWith` ResponseMatcher {
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Location" <:> [str|/json?data=eq.[1,2,3]|]]
}
describe "CSV insert" $ do
context "disparate csv types" $
it "succeeds with multipart response" $ do
pendingWith "Decide on what to do with CSV insert"
let inserted = [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
|]
request methodPost "/menagerie" [("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")] inserted
`shouldRespondWith` ResponseMatcher {
matchBody = Just inserted
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
}
context "requesting full representation" $ do
it "returns full details of inserted record" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nbar,baz"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "a,b\nbar,baz"
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Location" <:> "/no_pk?a=eq.bar&b=eq.baz"]
}
it "can post nulls" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nNULL,foo"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "a,b\n,foo"
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
}
context "with wrong number of columns" $
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
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" $
it "gives a 404" $
context "to unknown uri" $
it "gives a 404" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
context "to a known uri" $ do
context "without a fully-specified primary key" $
it "is not an allowed operation" $
it "is not an allowed operation" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/compound_pk?k1=eq.12" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 405
@@ -93,13 +238,15 @@ spec = afterAll_ resetDb $ around withApp $ do
context "with a fully-specified primary key" $ do
context "not specifying every column in the table" $
it "is rejected for lack of idempotence" $
it "is rejected for lack of idempotence" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 400
context "specifying every column in the table" . after_ (clearTable "compound_pk") $ do
context "specifying every column in the table" $ do
it "can create a new record" $ do
pendingWith "Decide on PUT usefullness"
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":3 } |]
liftIO $ do
@@ -116,6 +263,7 @@ spec = afterAll_ resetDb $ around withApp $ do
compoundExtra record `shouldBe` Just 3
it "can update an existing record" $ do
pendingWith "Decide on PUT usefullness"
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":4 } |]
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
@@ -128,9 +276,10 @@ spec = afterAll_ resetDb $ around withApp $ do
let record = head rows
compoundExtra record `shouldBe` Just 5
context "with an auto-incrementing primary key" . after_ (clearTable "auto_incrementing_pk") $
context "with an auto-incrementing primary key"$
it "succeeds with 204" $
it "succeeds with 204" $ do
pendingWith "Decide on PUT usefullness"
request methodPut "/auto_incrementing_pk?id=eq.1" []
[json| {
"id":1,
@@ -146,27 +295,30 @@ spec = afterAll_ resetDb $ around withApp $ do
describe "Patching record" $ do
context "to unkonwn uri" $
context "to unknown uri" $
it "gives a 404" $
request methodPatch "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
context "on an empty table" $
it "succeeds with no effect" $
request methodPatch "/simple_pk" []
it "indicates no records found to update" $
request methodPatch "/empty_table" []
[json| { "extra":20 } |]
`shouldRespondWith` 204
`shouldRespondWith` 404
context "in a nonempty table" . before_ (clearTable "items" >> createItems 15) .
after_ (clearTable "items") $ do
context "in a nonempty table" $ do
it "can update a single item" $ do
g <- get "/items?id=eq.42"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "\\*/0"
request methodPatch "/items?id=eq.1" []
request methodPatch "/items?id=eq.2" []
[json| { "id":42 } |]
`shouldRespondWith` 204
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "0-0/1"]
}
g' <- get "/items?id=eq.42"
liftIO $ simpleHeaders g'
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
@@ -182,3 +334,67 @@ spec = afterAll_ resetDb $ around withApp $ do
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
it "can set a column to NULL" $ do
_ <- post "/no_pk" [json| { a: "keepme", b: "nullme" } |]
_ <- request methodPatch "/no_pk?b=eq.nullme" [] [json| { b: null } |]
get "/no_pk?a=eq.keepme" `shouldRespondWith`
[json| [{ a: "keepme", b: null }] |]
it "can update based on a computed column" $
request methodPatch
"/items?always_true=eq.false"
[("Prefer", "return=representation")]
[json| { id: 100 } |]
`shouldRespondWith` 404
it "can provide a representation" $ do
_ <- post "/items"
[json| { id: 1 } |]
request methodPatch
"/items?id=eq.1"
[("Prefer", "return=representation")]
[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"
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
_ <- post "/postgrest/users" [json| { "id":"jroe", "pass": "1234", "role": "postgrest_test_author" } |]
p1 <- request methodPost "/authors_only"
[ auth, ("Prefer", "return=representation") ]
[json| { "secret": "nyancat" } |]
liftIO $ do
simpleBody p1 `shouldBe` [str|{"owner":"jdoe","secret":"nyancat"}|]
simpleStatus p1 `shouldBe` created201
p2 <- request methodPost "/authors_only"
-- jwt token for jroe
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.YuF_VfmyIxWyuceT7crnNKEprIYXsJAyXid3rjPjIow", ("Prefer", "return=representation") ]
[json| { "secret": "lolcat", "owner": "hacker" } |]
liftIO $ do
simpleBody p2 `shouldBe` [str|{"owner":"jroe","secret":"lolcat"}|]
simpleStatus p2 `shouldBe` created201
+29
View File
@@ -0,0 +1,29 @@
module Feature.QueryLimitedSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders, simpleStatus))
import SpecHelper
import Network.Wai (Application)
spec :: SpecWith Application
spec =
describe "Requesting many items with server limits enabled" $ do
it "restricts results" $
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3}] |]
, matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-2/15"]
}
it "respects additional client limiting" $ do
r <- request methodGet "/items"
(rangeHdrs $ ByteRangeFromTo 0 1) ""
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-1/15"
simpleStatus r `shouldBe` partialContent206
+403 -36
View File
@@ -1,30 +1,22 @@
module Feature.QuerySpec where
import Test.Hspec
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Hasql as H
import Hasql.Postgres as H
import Control.Monad (void)
import Data.Text(Text)
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders))
import SpecHelper
import Text.Heredoc
import Network.Wai (Application)
testSet :: IO ()
testSet = do
clearTable "items" >> clearTable "no_pk"
createItems 15
pool <- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $ H.tx Nothing $ do
H.unitEx $ insertNoPk "xyyx" "u"
H.unitEx $ insertNoPk "xYYx" "v"
spec :: SpecWith Application
spec = do
where
insertNoPk :: Text -> Text -> H.Stmt H.Postgres
insertNoPk = [H.stmt|insert into "1".no_pk (a, b) values (?,?)|]
describe "Querying a table with a column called count" $
it "should not confuse count column with pg_catalog.count aggregate" $
get "/has_count_column" `shouldRespondWith` 200
spec :: Spec
spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
describe "Querying a nonexistent table" $
it "causes a 404" $
get "/faketable" `shouldRespondWith` 404
@@ -38,19 +30,249 @@ spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
, matchHeaders = ["Content-Range" <:> "0-0/1"]
}
it "matches with equality using not operator" $
get "/items?id=not.eq.5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"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-13/14"]
}
it "matches with more than one condition using not operator" $
get "/simple_pk?k=like.*yx&extra=not.eq.u" `shouldRespondWith` "[]"
it "matches with inequality using not operator" $ do
get "/items?id=not.lt.14&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":14},{"id":15}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
get "/items?id=not.gt.2&order=id.asc"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":2}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "matches items IN" $
get "/items?id=in.1,3,5"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches items NOT IN" $
get "/items?id=notin.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches items NOT IN using not operator" $
get "/items?id=not.in.2,4,6,7,8,9,10,11,12,13,14,15"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1},{"id":3},{"id":5}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "matches nulls using not operator" $
get "/no_pk?a=not.is.null" `shouldRespondWith`
[json| [{"a":"1","b":"0"},{"a":"2","b":"0"}] |]
it "matches nulls in varchar and numeric fields alike" $ do
get "/no_pk?a=is.null" `shouldRespondWith`
[json| [{"a": null, "b": null}] |]
get "/nullable_integer?a=is.null" `shouldRespondWith` [str|[{"a":null}]|]
it "matches with like" $ do
get "/no_pk?a=like.*yx" `shouldRespondWith` [json|
[{"a":"xyyx","b":"u"}]|]
get "/no_pk?a=like.xy*" `shouldRespondWith` [json|
[{"a":"xyyx","b":"u"}]|]
get "/no_pk?a=like.*YY*" `shouldRespondWith` [json|
[{"a":"xYYx","b":"v"}]|]
get "/simple_pk?k=like.*yx" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"}]|]
get "/simple_pk?k=like.xy*" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"}]|]
get "/simple_pk?k=like.*YY*" `shouldRespondWith`
[str|[{"k":"xYYx","extra":"v"}]|]
it "matches with like using not operator" $
get "/simple_pk?k=not.like.*yx" `shouldRespondWith`
[str|[{"k":"xYYx","extra":"v"}]|]
it "matches with ilike" $ do
get "/no_pk?a=ilike.xy*&order=b.asc" `shouldRespondWith` [json|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
get "/no_pk?a=ilike.*YY*&order=b.asc" `shouldRespondWith` [json|
[{"a":"xyyx","b":"u"},{"a":"xYYx","b":"v"}]|]
get "/simple_pk?k=ilike.xy*&order=extra.asc" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]
get "/simple_pk?k=ilike.*YY*&order=extra.asc" `shouldRespondWith`
[str|[{"k":"xyyx","extra":"u"},{"k":"xYYx","extra":"v"}]|]
it "matches with ilike using not operator" $
get "/simple_pk?k=not.ilike.xy*&order=extra.asc" `shouldRespondWith` "[]"
it "matches with tsearch @@" $
get "/tsearch?text_search_vector=@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'bar':2 'foo':1"}] |]
it "matches with tsearch @@ using not operator" $
get "/tsearch?text_search_vector=not.@@.foo" `shouldRespondWith`
[json| [{"text_search_vector":"'baz':1 'qux':2"}] |]
it "matches with computed column" $
get "/items?always_true=eq.true&order=id.asc" `shouldRespondWith`
[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}] |]
it "order by computed column" $
get "/items?order=anti_id.desc" `shouldRespondWith`
[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}] |]
it "matches filtering nested items" $
get "/clients?select=id,projects{id,tasks{id,name}}&projects.tasks.name=like.Design*" `shouldRespondWith`
[str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1,"name":"Design w7"}]},{"id":2,"tasks":[{"id":3,"name":"Design w10"}]}]},{"id":2,"projects":[{"id":3,"tasks":[{"id":5,"name":"Design IOS"}]},{"id":4,"tasks":[{"id":7,"name":"Design OSX"}]}]}]|]
it "matches with @> operator" $
get "/complex_items?select=id&arr_data=@>.{2}" `shouldRespondWith`
[str|[{"id":2},{"id":3}]|]
it "matches with <@ operator" $
get "/complex_items?select=id&arr_data=<@.{1,2,4}" `shouldRespondWith`
[str|[{"id":1},{"id":2}]|]
describe "Shaping response with select parameter" $ do
it "selectStar works in absense of parameter" $
get "/complex_items?id=eq.3" `shouldRespondWith`
[str|[{"id":3,"name":"Three","settings":{"foo":{"int":1,"bar":"baz"}},"arr_data":[1,2,3]}]|]
it "one simple column" $
get "/complex_items?select=id" `shouldRespondWith`
[json| [{"id":1},{"id":2},{"id":3}] |]
it "one simple column with casting (text)" $
get "/complex_items?select=id::text" `shouldRespondWith`
[json| [{"id":"1"},{"id":"2"},{"id":"3"}] |]
it "json column" $
get "/complex_items?id=eq.1&select=settings" `shouldRespondWith`
[json| [{"settings":{"foo":{"int":1,"bar":"baz"}}}] |]
it "json subfield one level with casting (json)" $
get "/complex_items?id=eq.1&select=settings->>foo::json" `shouldRespondWith`
[json| [{"foo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
, matchStatus = 400
, matchHeaders = []
}
it "fails on bad casting (wrong cast type)" $
get "/complex_items?select=id::fakecolumntype"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"hint":null,"details":null,"code":"42704","message":"type \"fakecolumntype\" does not exist"} |]
, matchStatus = 400
, matchHeaders = []
}
it "json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
[json| [{"bar":"baz"}] |]
it "json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=settings->foo->>int::integer" `shouldRespondWith`
[json| [{"int":1}] |] -- the value in the db is an int, but here we expect a string for now
it "requesting parents and children" $
get "/projects?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
it "requesting parents and filtering parent columns" $
get "/projects?id=eq.1&select=id, name, clients{id}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1}}]|]
it "rows with missing parents are included" $
get "/projects?id=in.1,5&select=id,clients{id}" `shouldRespondWith`
[str|[{"id":1,"clients":{"id":1}},{"id":5,"clients":null}]|]
it "rows with no children return [] instead of null" $
get "/projects?id=in.5&select=id,tasks{id}" `shouldRespondWith`
[str|[{"id":5,"tasks":[]}]|]
it "requesting children 2 levels" $
get "/clients?id=eq.1&select=id,projects{id,tasks{id}}" `shouldRespondWith`
[str|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":3},{"id":4}]}]}]|]
it "requesting many<->many relation" $
get "/tasks?select=id,users{id}" `shouldRespondWith`
[str|[{"id":1,"users":[{"id":1},{"id":3}]},{"id":2,"users":[{"id":1}]},{"id":3,"users":[{"id":1}]},{"id":4,"users":[{"id":1}]},{"id":5,"users":[{"id":2},{"id":3}]},{"id":6,"users":[{"id":2}]},{"id":7,"users":[{"id":2}]},{"id":8,"users":[]}]|]
it "requesting many<->many relation reverse" $
get "/users?select=id,tasks{id}" `shouldRespondWith`
[str|[{"id":1,"tasks":[{"id":1},{"id":2},{"id":3},{"id":4}]},{"id":2,"tasks":[{"id":5},{"id":6},{"id":7}]},{"id":3,"tasks":[{"id":1},{"id":5}]}]|]
it "requesting parents and children on views" $
get "/projects_view?id=eq.1&select=id, name, clients{*}, tasks{id, name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}]|]
it "requesting children with composite key" $
get "/users_tasks?user_id=eq.2&task_id=eq.6&select=*, comments{content}" `shouldRespondWith`
[str|[{"user_id":2,"task_id":6,"comments":[{"content":"Needs to be delivered ASAP"}]}]|]
it "detect relations in views from exposed schema that are based on tables in private schema and have columns renames" $
get "/articles?id=eq.1&select=id,articleStars{users{*}}" `shouldRespondWith`
[str|[{"id":1,"articleStars":[{"users":{"id":1,"name":"Angela Martin"}},{"users":{"id":2,"name":"Michael Scott"}},{"users":{"id":3,"name":"Dwight Schrute"}}]}]|]
it "can select by column name" $
get "/projects?id=in.1,3&select=id,name,client_id,client_id{id,name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1,"client_id":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client_id":{"id":2,"name":"Apple"}}]|]
it "can select by column name sans id" $
get "/projects?id=in.1,3&select=id,name,client_id,client{id,name}" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1,"client":{"id":1,"name":"Microsoft"}},{"id":3,"name":"IOS","client_id":2,"client":{"id":2,"name":"Apple"}}]|]
describe "Plurality singular" $ do
it "will select an existing object" $
request methodGet "/items?id=eq.5" [("Prefer","plurality=singular")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"id":5} |]
, matchStatus = 200
, 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
request methodGet "/items" headers ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"id":1} |]
, matchStatus = 200
, matchHeaders = []
}
it "will respond with 404 when not found" $
request methodGet "/items?id=eq.9999" [("Prefer","plurality=singular")] ""
`shouldRespondWith` 404
it "can shape plurality singular object routes" $
request methodGet "/projects_view?id=eq.1&select=id,name,clients{*},tasks{id,name}" [("Prefer","plurality=singular")] ""
`shouldRespondWith`
[str|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|]
describe "ordering response" $ do
it "by a column asc" $
@@ -68,8 +290,63 @@ spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
, matchHeaders = ["Content-Range" <:> "0-1/2"]
}
it "by a column asc with nulls last" $
get "/no_pk?order=a.asc.nullslast"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"a":"1","b":"0"},
{"a":"2","b":"0"},
{"a":null,"b":null}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "by a column desc with nulls first" $
get "/no_pk?order=a.desc.nullsfirst"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"a":null,"b":null},
{"a":"2","b":"0"},
{"a":"1","b":"0"}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "by a column desc with nulls last" $
get "/no_pk?order=a.desc.nullslast"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"a":"2","b":"0"},
{"a":"1","b":"0"},
{"a":null,"b":null}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/3"]
}
it "without other constraints" $
get "/items?order=asc.id" `shouldRespondWith` 200
get "/items?order=id.asc" `shouldRespondWith` 200
describe "Accept headers" $ do
it "should respond an unknown accept type with 415" $
request methodGet "/simple_pk"
(acceptHdrs "text/unknowntype") ""
`shouldRespondWith` 415
it "should respond correctly to */* in accept header" $
request methodGet "/simple_pk"
(acceptHdrs "*/*") ""
`shouldRespondWith` 200
it "should respond correctly to multiple types in accept header" $
request methodGet "/simple_pk"
(acceptHdrs "text/unknowntype, text/csv") ""
`shouldRespondWith` 200
it "should respond with CSV to 'text/csv' request" $
request methodGet "/simple_pk"
(acceptHdrs "text/csv; version=1") ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just "k,extra\nxyyx,u\nxYYx,v"
, matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
}
describe "Canonical location" $ do
it "Sets Content-Location with alphabetized params" $
@@ -80,10 +357,100 @@ spec = beforeAll testSet . afterAll_ (clearTable "items") . around withApp $ do
, matchHeaders = ["Content-Location" <:> "/no_pk?a=eq.1&b=eq.1"]
}
it "Omits question mark when there are no params" $
get "/simple_pk"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Location" <:> "/simple_pk"]
}
it "Omits question mark when there are no params" $ do
r <- get "/simple_pk"
liftIO $ do
let respHeaders = simpleHeaders r
respHeaders `shouldSatisfy` matchHeader
"Content-Location" "/simple_pk"
describe "jsonb" $ do
it "can filter by properties inside json column" $ do
get "/json?data->foo->>bar=eq.baz" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |]
it "can filter by properties inside json column using not" $
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
[json| [] |]
it "can filter by properties inside json column using ->>" $
get "/json?data->>id=eq.1" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
describe "remote procedure call" $ do
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} ] |]
context "a proc that returns an empty rowset" $
it "returns empty json array" $
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
[json| [] |]
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`
[json| [{"so6meIdColumn":1},{"so6meIdColumn":2},{"so6meIdColumn":3},{"so6meIdColumn":4},{"so6meIdColumn":5}] |]
get "/ghostBusters" `shouldRespondWith`
[json| [{"escapeId":1},{"escapeId":3},{"escapeId":5}] |]
it "will embed a collection" $
get "/Escap3e;?select=ghostBusters{*}" `shouldRespondWith`
[json| [{"ghostBusters":[{"escapeId":1}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":3}]},{"ghostBusters":[]},{"ghostBusters":[{"escapeId":5}]}] |]
it "will embed using a column" $
get "/ghostBusters?select=escapeId{*}" `shouldRespondWith`
[json| [{"escapeId":{"so6meIdColumn":1}},{"escapeId":{"so6meIdColumn":3}},{"escapeId":{"so6meIdColumn":5}}] |]
+130 -5
View File
@@ -2,21 +2,146 @@ module Feature.RangeSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleHeaders,simpleStatus))
import qualified Data.ByteString.Lazy as BL
import SpecHelper
import Network.Wai (Application)
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
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" $
context "without range headers" $ do
context "with response under server size limit" $
it "returns whole range with status 200" $
get "/items" `shouldRespondWith` 200
context "when I don't want the count" $ do
it "returns range Content-Range with /*" $
request methodGet "/menagerie"
[("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns range Content-Range with range/*" $
request methodGet "/items?order=id"
[("Prefer", "count=none")] ""
`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/*"]
}
it "returns range Content-Range with range/* even using other filters" $
request methodGet "/items?id=eq.1&order=id"
[("Prefer", "count=none")] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| [{"id":1}] |]
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
context "with range headers" $ do
context "of acceptable range" $ do
+221 -28
View File
@@ -7,35 +7,112 @@ import Test.Hspec.Wai.JSON
import SpecHelper
import Network.HTTP.Types
import Network.Wai (Application)
spec :: SpecWith Application
spec = do
spec :: Spec
spec = around withApp $ do
describe "GET /" $ do
it "lists views in schema" $
request methodGet "/" [] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"auto_incrementing_pk","insertable":true}
, {"schema":"1","name":"compound_pk","insertable":true}
, {"schema":"1","name":"has_fk","insertable":true}
, {"schema":"1","name":"items","insertable":true}
, {"schema":"1","name":"menagerie","insertable":true}
, {"schema":"1","name":"no_pk","insertable":true}
, {"schema":"1","name":"simple_pk","insertable":true}
{"schema":"test","name":"Escap3e;","insertable":true}
, {"schema":"test","name":"articleStars","insertable":true}
, {"schema":"test","name":"articles","insertable":true}
, {"schema":"test","name":"auto_incrementing_pk","insertable":true}
, {"schema":"test","name":"clients","insertable":true}
, {"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}
, {"schema":"test","name":"insertable_view_with_join","insertable":true}
, {"schema":"test","name":"insertonly","insertable":true}
, {"schema":"test","name":"items","insertable":true}
, {"schema":"test","name":"json","insertable":true}
, {"schema":"test","name":"materialized_view","insertable":false}
, {"schema":"test","name":"menagerie","insertable":true}
, {"schema":"test","name":"no_pk","insertable":true}
, {"schema":"test","name":"nullable_integer","insertable":true}
, {"schema":"test","name":"projects","insertable":true}
, {"schema":"test","name":"projects_view","insertable":true}
, {"schema":"test","name":"simple_pk","insertable":true}
, {"schema":"test","name":"tasks","insertable":true}
, {"schema":"test","name":"tsearch","insertable":true}
, {"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}
it "lists only views user has permission to see" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeader "jdoe" "1234"
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/" [auth] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true}
{"schema":"test","name":"authors_only","insertable":true}
] |]
{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|
@@ -46,7 +123,7 @@ spec = around withApp $ do
"default": null,
"precision": 32,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "integer",
"type": "integer",
"maxLen": null,
@@ -59,7 +136,7 @@ spec = around withApp $ do
"default": null,
"precision": 53,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "double",
"type": "double precision",
"maxLen": null,
@@ -71,7 +148,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "varchar",
"type": "character varying",
"maxLen": null,
@@ -84,7 +161,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "boolean",
"type": "boolean",
"maxLen": null,
@@ -96,7 +173,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "date",
"type": "date",
"maxLen": null,
@@ -108,7 +185,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "money",
"type": "money",
"maxLen": null,
@@ -121,7 +198,7 @@ spec = around withApp $ do
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "enum",
"type": "USER-DEFINED",
"maxLen": null,
@@ -138,19 +215,72 @@ spec = around withApp $ do
}
|]
it "includes foreign key data" $ do
pendingWith "have to resolve issue #107"
it "it includes primary and foreign keys for views" $
request methodOptions "/projects_view" [] "" `shouldRespondWith`
[json|
{
"pkey":[
"id"
],
"columns":[
{
"references":null,
"default":null,
"precision":32,
"updatable":true,
"schema":"test",
"name":"id",
"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":"clients"
},
"default":null,
"precision":32,
"updatable":true,
"schema":"test",
"name":"client_id",
"type":"integer",
"maxLen":null,
"enum":[],
"nullable":true,
"position":3
}
]
}
|]
it "includes foreign key data" $
request methodOptions "/has_fk" [] ""
`shouldRespondWith` [json|
{
"pkey": ["id"],
"columns":[
{
"default": "nextval('\"1\".has_fk_id_seq'::regclass)",
"default": "nextval('test.has_fk_id_seq'::regclass)",
"precision": 64,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "id",
"type": "bigint",
"maxLen": null,
@@ -162,27 +292,90 @@ spec = around withApp $ do
"default": null,
"precision": 32,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "auto_inc_fk",
"type": "integer",
"maxLen": null,
"nullable": true,
"position": 2,
"enum": [],
"references": {"table": "auto_incrementing_pk", "column": "id"}
"references": {"schema":"test", "table": "auto_incrementing_pk", "column": "id"}
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"schema": "test",
"name": "simple_fk",
"type": "character varying",
"maxLen": 255,
"nullable": true,
"position": 3,
"enum": [],
"references": {"table": "simple_pk", "column": "k"}
"references": {"schema":"test", "table": "simple_pk", "column": "k"}
}
]
}
|]
it "includes all information on views for renamed columns, and raises relations to correct schema" $
request methodOptions "/articleStars" [] ""
`shouldRespondWith` [json|
{
"pkey": [
"articleId",
"userId"
],
"columns": [
{
"references": {
"schema": "test",
"column": "id",
"table": "articles"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "articleId",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 1
},
{
"references": {
"schema": "test",
"column": "id",
"table": "users"
},
"default": null,
"precision": 32,
"updatable": true,
"schema": "test",
"name": "userId",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 2
},
{
"references": null,
"default": null,
"precision": null,
"updatable": true,
"schema": "test",
"name": "createdAt",
"type": "timestamp without time zone",
"maxLen": null,
"enum": [],
"nullable": true,
"position": 3
}
]
}
|]
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 }] |]
+50 -2
View File
@@ -2,7 +2,55 @@ module Main where
import Test.Hspec
import SpecHelper
import Spec
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
import qualified Feature.QueryLimitedSpec
import qualified Feature.QuerySpec
import qualified Feature.RangeSpec
import qualified Feature.StructureSpec
import qualified Feature.UnicodeSpec
main :: IO ()
main = resetDb >> hspec spec
main = do
setupDb
pool <- P.acquire (3, 10, cs testDbConn)
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 = 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)
]
-1
View File
@@ -1 +0,0 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --no-main #-}
+29 -84
View File
@@ -1,90 +1,55 @@
module SpecHelper where
import Network.Wai
import Test.Hspec
import Test.Hspec.Wai
import Hasql as H
import Hasql.Backend as H
import Hasql.Postgres as H
import Data.String.Conversions (cs)
import Data.Monoid
import Data.Text hiding (map)
import qualified Data.Vector as V
import Control.Monad (void)
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
hRange, hAuthorization)
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 Network.Wai.Middleware.Cors (cors)
import System.Process (readProcess)
import Web.JWT (secret)
import App (app)
import Config (AppConfig(..), corsPolicy)
import Middleware
import Error(errResponse)
-- import Auth (addUser)
import PostgREST.Config (AppConfig(..))
isLeft :: Either a b -> Bool
isLeft (Left _ ) = True
isLeft _ = False
testDbConn :: String
testDbConn = "postgres://postgrest_test_authenticator@localhost:5432/postgrest_test"
cfg :: AppConfig
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10
testCfg :: AppConfig
testCfg =
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 Nothing True
testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
testUnicodeCfg :: AppConfig
testUnicodeCfg =
AppConfig testDbConn "postgrest_test_anonymous" "تست" 3000 (secret "safe") 10 Nothing True
pgSettings :: H.Settings
pgSettings = H.ParamSettings (cs $ configDbHost cfg)
(fromIntegral $ configDbPort cfg)
(cs $ configDbUser cfg)
(cs $ configDbPass cfg)
(cs $ configDbName cfg)
withApp :: ActionWith Application -> IO ()
withApp perform = do
let anonRole = cs $ configAnonRole cfg
currRole = cs $ configDbUser cfg
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
perform $ middle $ \req resp -> do
body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx Nothing
$ authenticated currRole anonRole (app body) req
either (resp . errResponse) resp result
where middle = cors corsPolicy
resetDb :: IO ()
resetDb = do
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $
H.tx Nothing $ do
H.unitEx [H.stmt| drop schema if exists "1" cascade |]
H.unitEx [H.stmt| drop schema if exists private cascade |]
H.unitEx [H.stmt| drop schema if exists postgrest cascade |]
testLtdRowsCfg :: AppConfig
testLtdRowsCfg =
AppConfig testDbConn "postgrest_test_anonymous" "test" 3000 (secret "safe") 10 (Just 3) True
setupDb :: IO ()
setupDb = do
void $ readProcess "psql" ["-d", "postgres", "-a", "-f", "test/fixtures/database.sql"] []
loadFixture "roles"
loadFixture "schema"
loadFixture "privileges"
resetDb
resetDb :: IO ()
resetDb = loadFixture "data"
loadFixture :: FilePath -> IO()
loadFixture name =
void $ readProcess "psql" ["-U", "postgrest_test", "-d", "postgrest_test", "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
rangeHdrs :: ByteRange -> [Header]
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
acceptHdrs :: BS.ByteString -> [Header]
acceptHdrs mime = [(hAccept, mime)]
rangeUnit :: Header
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
@@ -92,30 +57,10 @@ matchHeader :: CI BS.ByteString -> String -> [Header] -> Bool
matchHeader name valRegex headers =
maybe False (=~ valRegex) $ lookup name headers
authHeader :: String -> String -> Header
authHeader u p =
authHeaderBasic :: String -> String -> Header
authHeaderBasic u p =
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
clearTable :: Text -> IO ()
clearTable table = do
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ H.Stmt ("delete from \"1\"."<>table) V.empty True
createItems :: Int -> IO ()
createItems n = do
pool :: H.Pool H.Postgres
<- H.acquirePool pgSettings testPoolOpts
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = sequence_ $ map H.unitEx stmts
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
-- for hspec-wai
pending_ :: WaiSession ()
pending_ = liftIO Test.Hspec.pending
-- for hspec-wai
pendingWith_ :: String -> WaiSession ()
pendingWith_ = liftIO . Test.Hspec.pendingWith
authHeaderJWT :: String -> Header
authHeaderJWT token =
(hAuthorization, cs $ "Bearer " ++ token)
+3 -1
View File
@@ -8,9 +8,11 @@ module TestTypes (
import qualified Data.Aeson as JSON
import Data.Aeson ((.:))
-- import Data.Maybe (fromJust)
import Control.Applicative ((<$>), (<*>))
import Control.Applicative
import Control.Monad (mzero)
import Prelude
data IncPK = IncPK {
incId :: Int
, incNullableStr :: Maybe String
@@ -1,7 +1,7 @@
module Unit.PgStructureSpec where
module Unit.DbStructureSpec where
import Test.Hspec
import PgStructure (Table(..), tables, Column(..), columns, ForeignKey(..),
import DbStructure (Table(..), tables, Column(..), columns, ForeignKey(..),
foreignKeys)
import Database.HDBC (quickQuery)
@@ -12,25 +12,25 @@ spec :: Spec
spec = around dbWithSchema $ beforeWith setRole $ do
describe "tables" $
it "shows all the tables" $ \conn -> do
ts <- tables "1" conn
ts <- tables "test" conn
map tableName ts `shouldBe` ["authors_only","auto_incrementing_pk",
"compound_pk","has_fk","items","menagerie","no_pk", "simple_pk"]
"compound_pk","has_fk","insertable_view_with_join","items","menagerie","no_pk", "simple_pk"]
describe "columns" $ do
it "responds with each column for the table" $ \conn -> do
cs <- columns "1" "auto_incrementing_pk" conn
cs <- columns "test" "auto_incrementing_pk" conn
map colName cs `shouldBe` ["id","nullable_string","non_nullable_string",
"inserted_at"]
it "includes foreign key data" $ \conn -> do
cs <- columns "1" "has_fk" conn
cs <- columns "test" "has_fk" conn
map colFK cs `shouldBe` [Nothing,
Just $ ForeignKey "auto_incrementing_pk" "id",
Just $ ForeignKey "simple_pk" "k"]
describe "foreignKeys" $
it "has a description of the foreign key columns" $ \conn ->
foreignKeys "1" "has_fk" conn `shouldReturn` M.fromList [
foreignKeys "test" "has_fk" conn `shouldReturn` M.fromList [
("auto_inc_fk", ForeignKey {fkTable="auto_incrementing_pk", fkCol="id"}),
("simple_fk", ForeignKey { fkTable="simple_pk", fkCol="k"})]
+7 -7
View File
@@ -32,7 +32,7 @@ spec = around dbWithSchema $ do
describe "insert" $
describe "with an auto-increment key" $ do
it "inserts and responds with a full object description" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
r <- insert "test" "auto_incrementing_pk" (SqlRow [
("non_nullable_string", toSql ("a string"::String))]) conn
let returnRow = incFromList . toList $ r
incStr returnRow `shouldBe` "a string"
@@ -43,24 +43,24 @@ spec = around dbWithSchema $ do
[returnRow] `shouldBe` map incFromList tRows
it "throws an exception if the PK is not unique" $ \conn -> do
r <- insert "1" "auto_incrementing_pk" (SqlRow [
r <- insert "test" "auto_incrementing_pk" (SqlRow [
("non_nullable_string", toSql ("a string"::String))]) conn
let row = SqlRow . map (Control.Arrow.first cs) . toList $ r
insert "1" "auto_incrementing_pk" row conn `shouldThrow` \e ->
insert "test" "auto_incrementing_pk" row conn `shouldThrow` \e ->
seState e == "23505" -- uniqueness violation code
it "throws an exception if a required value is missing" $ \conn ->
insert "1" "auto_incrementing_pk" (SqlRow [
insert "test" "auto_incrementing_pk" (SqlRow [
("nullable_string", toSql ("a string"::String))]) conn
`shouldThrow` \e -> seState e == "23502"
it "generates a default values query if no data is provided" $ \c -> do
r <- insert "1" "items" (SqlRow []) c
r <- insert "test" "items" (SqlRow []) c
let [row] = toList r
quickALQuery c "select * from \"1\".items where id = ?" [snd row]
`shouldReturn` [[row]]
let {user = "jdoe"; pass = "secret"; role = "test_default_role"}
let {user = "jdoe"; pass = "secret"; role = "postgrest_test_default_role"}
describe "addUser" $ do
it "adds a correct user to the right table" $ \conn -> do
addUser user pass role conn
@@ -79,7 +79,7 @@ spec = around dbWithSchema $ do
addUser user pass role conn
return conn) $ do
it "accepts correct credentials and return the role" $ \conn ->
signInRole user pass conn `shouldReturn` LoginSuccess role
signInRole user pass conn `shouldReturn` LoginSuccess role user
it "returns nothing with bad creds" $ \conn -> do
signInRole "not-a-user" pass conn `shouldReturn` LoginFailed
+273
View File
@@ -0,0 +1,273 @@
--
-- PostgreSQL database dump
--
-- Dumped from database version 9.5beta1
-- Dumped by pg_dump version 9.5beta1
SET statement_timeout = 0;
SET lock_timeout = 0;
SET client_encoding = 'UTF8';
SET standard_conforming_strings = on;
SET check_function_bodies = false;
SET client_min_messages = warning;
SET search_path = postgrest, pg_catalog;
--
-- Data for Name: auth; Type: TABLE DATA; Schema: postgrest; Owner: -
--
TRUNCATE TABLE auth CASCADE;
INSERT INTO auth VALUES ('jdoe', 'postgrest_test_author', '1234 ');
SET search_path = private, pg_catalog;
--
-- Data for Name: articles; Type: TABLE DATA; Schema: private; Owner: -
--
TRUNCATE TABLE articles CASCADE;
INSERT INTO articles VALUES (1, 'No… It''s a thing; it''s like a plan, but with more greatness.', 'diogo');
INSERT INTO articles VALUES (2, 'Stop talking, brain thinking. Hush.', 'diogo');
INSERT INTO articles VALUES (3, 'It''s a fez. I wear a fez now. Fezes are cool.', 'diogo');
SET search_path = test, pg_catalog;
--
-- Data for Name: users; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE users CASCADE;
INSERT INTO users VALUES (1, 'Angela Martin');
INSERT INTO users VALUES (2, 'Michael Scott');
INSERT INTO users VALUES (3, 'Dwight Schrute');
SET search_path = private, pg_catalog;
--
-- Data for Name: article_stars; Type: TABLE DATA; Schema: private; Owner: -
--
TRUNCATE TABLE article_stars CASCADE;
INSERT INTO article_stars VALUES (1, 1, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (1, 2, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (2, 3, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (3, 2, '2015-12-08 04:22:57.472738');
INSERT INTO article_stars VALUES (1, 3, '2015-12-08 04:22:57.472738');
SET search_path = test, pg_catalog;
--
-- Data for Name: authors_only; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE authors_only CASCADE;
--
-- Data for Name: auto_incrementing_pk; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE auto_incrementing_pk CASCADE;
--
-- Name: auto_incrementing_pk_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('auto_incrementing_pk_id_seq', 1, true);
--
-- Data for Name: clients; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE clients CASCADE;
INSERT INTO clients VALUES (1, 'Microsoft');
INSERT INTO clients VALUES (2, 'Apple');
--
-- Data for Name: projects; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE projects CASCADE;
INSERT INTO projects VALUES (1, 'Windows 7', 1);
INSERT INTO projects VALUES (2, 'Windows 10', 1);
INSERT INTO projects VALUES (3, 'IOS', 2);
INSERT INTO projects VALUES (4, 'OSX', 2);
INSERT INTO projects VALUES (5, 'Orphan', NULL);
--
-- Data for Name: tasks; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE tasks CASCADE;
INSERT INTO tasks VALUES (1, 'Design w7', 1);
INSERT INTO tasks VALUES (2, 'Code w7', 1);
INSERT INTO tasks VALUES (3, 'Design w10', 2);
INSERT INTO tasks VALUES (4, 'Code w10', 2);
INSERT INTO tasks VALUES (5, 'Design IOS', 3);
INSERT INTO tasks VALUES (6, 'Code IOS', 3);
INSERT INTO tasks VALUES (7, 'Design OSX', 4);
INSERT INTO tasks VALUES (8, 'Code OSX', 4);
--
-- Data for Name: users_tasks; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE users_tasks CASCADE;
INSERT INTO users_tasks VALUES (1, 1);
INSERT INTO users_tasks VALUES (1, 2);
INSERT INTO users_tasks VALUES (1, 3);
INSERT INTO users_tasks VALUES (1, 4);
INSERT INTO users_tasks VALUES (2, 5);
INSERT INTO users_tasks VALUES (2, 6);
INSERT INTO users_tasks VALUES (2, 7);
INSERT INTO users_tasks VALUES (3, 1);
INSERT INTO users_tasks VALUES (3, 5);
--
-- Data for Name: comments; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE comments CASCADE;
INSERT INTO comments VALUES (1, 1, 2, 6, 'Needs to be delivered ASAP');
--
-- Data for Name: complex_items; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE complex_items CASCADE;
INSERT INTO complex_items VALUES (1, 'One', '{"foo":{"int":1,"bar":"baz"}}', '{1}');
INSERT INTO complex_items VALUES (2, 'Two', '{"foo":{"int":1,"bar":"baz"}}', '{1,2}');
INSERT INTO complex_items VALUES (3, 'Three', '{"foo":{"int":1,"bar":"baz"}}', '{1,2,3}');
--
-- Data for Name: compound_pk; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE compound_pk CASCADE;
--
-- Data for Name: simple_pk; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE simple_pk CASCADE;
INSERT INTO simple_pk VALUES ('xyyx', 'u');
INSERT INTO simple_pk VALUES ('xYYx', 'v');
--
-- Data for Name: has_fk; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE has_fk CASCADE;
--
-- Name: has_fk_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('has_fk_id_seq', 1, false);
--
-- Data for Name: items; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE items CASCADE;
INSERT INTO items VALUES (1);
INSERT INTO items VALUES (2);
INSERT INTO items VALUES (3);
INSERT INTO items VALUES (4);
INSERT INTO items VALUES (5);
INSERT INTO items VALUES (6);
INSERT INTO items VALUES (7);
INSERT INTO items VALUES (8);
INSERT INTO items VALUES (9);
INSERT INTO items VALUES (10);
INSERT INTO items VALUES (11);
INSERT INTO items VALUES (12);
INSERT INTO items VALUES (13);
INSERT INTO items VALUES (14);
INSERT INTO items VALUES (15);
--
-- Name: items_id_seq; Type: SEQUENCE SET; Schema: test; Owner: -
--
SELECT pg_catalog.setval('items_id_seq', 1, true);
--
-- Data for Name: json; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE json CASCADE;
INSERT INTO json VALUES ('{"foo":{"bar":"baz"},"id":1}');
--
-- Data for Name: menagerie; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE menagerie CASCADE;
--
-- Data for Name: no_pk; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE no_pk CASCADE;
INSERT INTO no_pk VALUES (NULL, NULL);
INSERT INTO no_pk VALUES ('1', '0');
INSERT INTO no_pk VALUES ('2', '0');
--
-- Data for Name: nullable_integer; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE nullable_integer CASCADE;
INSERT INTO nullable_integer VALUES (NULL);
--
-- Data for Name: tsearch; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE tsearch CASCADE;
INSERT INTO tsearch VALUES ('''bar'':2 ''foo'':1');
INSERT INTO tsearch VALUES ('''baz'':1 ''qux'':2');
--
-- Data for Name: users_projects; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE users_projects CASCADE;
INSERT INTO users_projects VALUES (1, 1);
INSERT INTO users_projects VALUES (1, 2);
INSERT INTO users_projects VALUES (2, 3);
INSERT INTO users_projects VALUES (2, 4);
INSERT INTO users_projects VALUES (3, 1);
INSERT INTO users_projects VALUES (3, 3);
TRUNCATE TABLE "Escap3e;" CASCADE;
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
--
+4
View File
@@ -0,0 +1,4 @@
DROP DATABASE IF EXISTS postgrest_test;
DROP ROLE IF EXISTS postgrest_test;
CREATE USER postgrest_test createdb createrole;
CREATE DATABASE postgrest_test OWNER postgrest_test;
+53
View File
@@ -0,0 +1,53 @@
-- Privileges for anonymous
GRANT USAGE ON SCHEMA
postgrest
, test
, "تست"
TO postgrest_test_anonymous;
-- Schema test objects
SET search_path = test, "تست", pg_catalog;
GRANT ALL ON TABLE
items
, "articleStars"
, articles
, auto_incrementing_pk
, clients
, comments
, complex_items
, compound_pk
, has_count_column
, has_fk
, insertable_view_with_join
, json
, materialized_view
, menagerie
, no_pk
, nullable_integer
, projects
, 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;
GRANT USAGE ON SEQUENCE
auto_incrementing_pk_id_seq
, items_id_seq
, callcounter_count
TO postgrest_test_anonymous;
-- Privileges for non anonymous users
GRANT USAGE ON SCHEMA test TO postgrest_test_author;
GRANT ALL ON TABLE authors_only TO postgrest_test_author;
+6 -15
View File
@@ -1,16 +1,7 @@
create function pg_temp.create_role_if_not_exists(rolename name, opts character varying) RETURNS text
LANGUAGE plpgsql
AS $$
BEGIN
IF NOT EXISTS (SELECT * FROM pg_roles WHERE rolname = rolename) THEN
EXECUTE format('CREATE ROLE %I %s', rolename, opts);
RETURN 'CREATE ROLE';
ELSE
RETURN format('ROLE ''%I'' ALREADY EXISTS', rolename);
END IF;
END;
$$;
DROP ROLE IF EXISTS postgrest_test_authenticator, postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author;
CREATE ROLE postgrest_test_authenticator WITH login noinherit;
CREATE ROLE postgrest_test_anonymous;
CREATE ROLE postgrest_test_default_role;
CREATE ROLE postgrest_test_author;
select pg_temp.create_role_if_not_exists('postgrest_anonymous', 'with nologin') as a
, pg_temp.create_role_if_not_exists('test_default_role', 'with nologin') as b
, pg_temp.create_role_if_not_exists('postgrest_test_author', 'with nologin') into temp shh;
GRANT postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author TO postgrest_test_authenticator;
+676 -496
View File
File diff suppressed because it is too large Load Diff