Compare commits

..
938 Commits
Author SHA1 Message Date
steve-chavez 1e732ac94a Bump to v5.2.0 2018-12-12 18:59:10 -05:00
steve-chavez 6b2778749f Add ability to quote column names on filters 2018-12-12 12:07:30 -05:00
steve-chavez 36f86827ee Fix #1008, Handle columns that contain spaces 2018-12-12 12:07:30 -05:00
steve-chavez d78410473e Add test for embedding on a view with group by 2018-12-12 12:07:30 -05:00
Steve ChávezandGitHub 501edc718d Add db-extra-search-path config (#1218)
For adding schemas to the search_path, solves issues related to extensions created in the public schema.
2018-12-08 11:39:31 -05:00
周丰andSteve Chávez 0d6d112b38 Read db-uri configuration from a separate file. (#1215)
* Read dburi configuration from a separate file.

* Add changelog entry.
2018-12-04 13:58:29 -05:00
Russell DaviesandSteve Chávez 473ac70789 Add support for parsing JSON Web Key Sets 2018-11-13 13:58:17 -05:00
steve-chavez dadfe965b9 Do hlint 2.1 hints 2018-11-06 11:54:10 -05:00
steve-chavez 63ead89470 Fix #1181, correct qualify of function argument type 2018-11-06 11:54:10 -05:00
steve-chavez ab23ed7999 Add CHANGELOG entry for allowing CORS on PUT 2018-10-21 13:38:34 -05:00
Kyle RussellandSteve Chávez 2da6bd6d1c Config: apply CorsResourcePolicy to PUT requests
PUT requests are part of the public upsert API, so if postgrest
runs at a different origin, these requests will fail because they
don't pass CORS.
2018-10-21 13:30:52 -05:00
steve-chavez 5bfb68b982 Fix #1180, embedding on views with composite pks
Also add CHANGELOG entries for previous fixes.
2018-10-12 09:24:22 -05:00
steve-chavez dc834572d6 Fix #1182, fix subselect view embedding for pg10 2018-10-12 09:24:22 -05:00
steve-chavez b48824bddd Fix #1065, correct test for pg10
Also add pg10 testing job on CircleCI
2018-10-12 09:24:22 -05:00
steve-chavez 6d326fe341 Update README.md 2018-10-12 09:22:58 -05:00
Roger LAMandSteve Chávez d94cf2ed72 tiny typo? 2018-10-11 09:00:27 -05:00
steve-chavez 27ca6b4e90 Pin hasql versions in cabal and stack 2018-09-28 08:57:26 -05:00
Ben GamariandSteve Chávez e0cc4d1571 Update hasql
Move to hasql 1.3.
2018-09-28 08:57:26 -05:00
steve-chavez 3cef4b70b0 Bump to v5.1.0 2018-08-31 22:09:03 -05:00
steve-chavez 6f97c34a86 Increase memory usage limit for tests
Sometimes there's more memory usage when running the tests on CircleCI
2018-08-31 22:09:03 -05:00
steve-chavez bdac90491d Add CHANGELOG and better categories to cabal file
Also update appveyor.yml
2018-08-31 22:09:03 -05:00
steve-chavez 5961f7a116 Update appveyor.yml 2018-08-31 09:09:17 -05:00
steve-chavez 17cd2725fd Pin hasql, protolude and jose versions in cabal 2018-08-31 09:09:17 -05:00
steve-chavez 5e7606134a Restrict base package version and cabal additions
There are issues with ghc 8.2 and 8.4,
see https://github.com/PostgREST/postgrest/issues/1109
2018-08-31 09:09:17 -05:00
mdr1384andSteve Chávez dfa9055c34 Close #1158, Add summary to OpenAPI doc for RPC functions (#1170) 2018-08-27 10:22:21 -05:00
steve-chavez 6907e7f979 Go back to *4 host for docker
127.0.0.1 cause issues on macos/windows
2018-08-23 13:03:12 -05:00
steve-chavez 8cf68c63d9 Fix segmentation fault/access violation on windows 10
This was reported in https://gitter.im/begriffs/postgrest?at=5b7431eb5b07ae730ac083af
and according to https://ghc.haskell.org/trac/ghc/ticket/13112#comment:25
this is an issue with linking in postgresql-libpq. The 0.9.4.1 version
contains a patch(https://github.com/lpsmith/postgresql-libpq/pull/45) that fixes the issue.
2018-08-21 11:27:44 -05:00
Steve ChávezandGitHub 30b5859b28 Fix #1141, app.settings resetting on pool timeout (#1169)
* Add test for ensuring app.settings don't reset
2018-08-20 11:02:03 -05:00
Steve ChávezandGitHub 0a1d83ce8f Fix JWTIssuedAtFuture for valid iat claim (#1166)
* Add test for ensuring "iat" works with time cache
2018-08-16 12:29:32 -05:00
steve-chavez d7511a2637 Add SIGUSR1 as another signal for schema reloading 2018-08-16 11:15:09 -05:00
steve-chavez 2066220244 Change server-host default of *4 to 127.0.0.1 2018-08-16 11:15:09 -05:00
steve-chavez 93f10adb3c Fix #905, intermittent empty replies 2018-08-09 08:59:19 -05:00
mdr1384andSteve Chávez 56bd5d5f91 Quote identifier names in RPC function arguments (#1153) 2018-07-30 09:11:09 -05:00
Alexey AlekhinandSteve Chávez 70e95649fd OpenAPI: Change GET response type to array (#1149) 2018-07-24 12:26:45 -05:00
Alexey AlekhinandSteve Chávez 2b46afe1ec OpenAPI: add required section for the non-nullable columns (#1148) 2018-07-23 11:11:45 -05:00
steve-chavez fa1e92fdf2 Add Lead Backer. Thank you Michel. 2018-07-19 11:05:46 -05:00
steve-chavez b1a8bd2391 Close #709, add test for embedding a view with CTE
Also add test for embedding a view with subselect in FROM clause
2018-07-18 09:43:42 -05:00
steve-chavez 69a76a627f Close #1145, OpenAPI materialized view columns 2018-07-18 09:43:42 -05:00
steve-chavez 105671e51a Fix #1129, view embedding on capitalized table
- Now also works on camelCase fk column
2018-07-18 09:43:42 -05:00
steve-chavez ecf0e9213f Remove array slice in allSynonyms query
This is for ensuring compatibility with pg 9.4 which doesn't support
omitting the array slice upper bound, [2:] gives an error.

The query still works fine because the array slice was to avoid having
a "({" result but this gets removed in the later join.
2018-07-18 09:43:42 -05:00
steve-chavez 1c6ded16d1 Add improved query for allSynonyms
This query works with views with subselects, and it's a lot faster,
on a complex schema the previous query was taking around 8 mins,
this query takes less than half a second(403.072 ms).

Also reorder view embedding tests
2018-07-18 09:43:42 -05:00
steve-chavez 6fc9d5191a Move create/destroy db scripts to bash
There was a "[[: not found" error with sh
2018-06-22 10:48:54 -05:00
steve-chavez 80f09780cc Add logo and db uri example to heroku button 2018-06-21 11:04:31 -05:00
steve-chavez 9e3454129f Add tests for jsonb equality filtering 2018-06-19 11:17:59 -05:00
steve-chavez d34afe861a Allow finishing a json path with single arrow ->
Now a json can be obtained without resorting to casting

- Previously: `/json_arr?select=data->>2::json`
- Now: `/json_arr?select=data->2`
2018-06-19 11:17:59 -05:00
steve-chavez 30dfadec7b Add support for getting json by array index
* Also support json negative array index
2018-06-19 11:17:59 -05:00
steve-chavez 2513c00039 Refactor Field type,move tests to JsonOperatorSpec 2018-06-19 11:17:59 -05:00
steve-chavez 100bf494ac Move database creation/destruction scripts to sh
Bash is not available on /bin/bash on some linux distros like NixOS
2018-06-19 11:17:59 -05:00
steve-chavez e8188b0d41 Add Lead Backer. Thank you Daniel. 2018-06-18 11:49:15 -05:00
steve-chavez 3958ebbb05 Add CHANGELOG note about stored procedures 2018-05-31 11:57:42 -05:00
steve-chavez 37e7398a85 Fix UPSERT bug on camelCase PK column 2018-05-31 11:57:42 -05:00
Felix YanandSteve Chávez 9ea7529f30 Use protolude 0.2.2, replace undefined with witness
protolude provides headMay since 0.2.2, it can be used instead of safe.
2018-05-28 13:08:27 -05:00
ilovezfsandSteve Chávez 384767708b postgrest.cabal: fix constraints on protolude and hasql-transaction 2018-05-27 08:54:46 -05:00
steve-chavez 6bcbb124d2 Add PGRST_ROLE_CLAIM_KEY to ENV 2018-05-25 10:27:09 -05:00
DominikandSteve Chávez f6c1ff810e Add role-claim-key to docker postgrest.conf
With #1091, the role-claim-key was added, but it is not configurable in docker containers as the template config doesn't allow it.
This patch adds the necessary line to the postgrest.conf of the docker container.
2018-05-25 09:26:44 -05:00
steve-chavez f80cfbf165 Add BACKERS.md 2018-05-21 13:11:24 -05:00
steve-chavez d8896be2c1 Add more details to issue template 2018-05-16 09:25:24 -05:00
steve-chavez 903a8d5f5a Bump to v0.5.0.0 2018-05-14 19:08:51 -05:00
steve-chavez ca76a8e6be Update links, add patreon badge 2018-05-14 18:44:52 -05:00
steve-chavez 28845e0f43 Fix accepting misspellings in order syntax 2018-05-14 07:34:15 -05:00
steve-chavez 30cf1d100a Add foreign tables to OpenAPI output 2018-05-07 10:14:26 -05:00
Joe NelsonandGitHub 05180f6539 Remove consulting prompt in issue template 2018-05-04 16:55:07 -05:00
steve-chavez b00f57ac34 Fix appveyor build
Appveyor failed on latest release https://ci.appveyor.com/project/begriffs/postgrest/build/1.0.5
according to https://github.com/commercialhaskell/stack/issues/2617#issuecomment-271966495
this can be fixed by adding `-j1`.
2018-05-02 13:06:44 -05:00
steve-chavez e9aaf05335 Remove support for non url safe operators 2018-05-02 13:06:44 -05:00
steve-chavez 79a7ce49f2 Remove support for {} embed and "in" op w/o parens 2018-05-02 13:06:44 -05:00
steve-chavez 3a1213f53b Add CHANGELOG entry for role-claim-key 2018-04-30 11:38:56 -05:00
steve-chavez f033c2c4b5 Add role-claim-key config value 2018-04-30 11:31:06 -05:00
steve-chavez 5c87fe2704 Add getCurrentTime cache for jwt validation 2018-04-30 11:31:06 -05:00
Joe Nelson 58f4b4bc33 Issue template 2018-04-12 21:28:11 -05:00
steve-chavez 32c7e32bdf Allow embeds alias to be used in filters, fix #821 2018-04-02 11:09:45 -05:00
steve-chavez 062a5581f5 Allow specifying source column in embed, fix #1078 2018-04-02 11:09:45 -05:00
steve-chavez 50512e1117 Refactor addRelations and findRelation functions
Just reordering and renaming variables to reduce indentation, logic remains the same.
2018-04-02 11:09:45 -05:00
steve-chavez edae60f8c1 Only alias tables on self join cases 2018-04-02 11:09:45 -05:00
steve-chavez 243e692192 Refactor some functions to use concatMap 2018-03-17 07:53:41 -05:00
steve-chavez 349a5ae076 Fix Parent Relation when having many views(#1044) 2018-03-17 07:53:41 -05:00
steve-chavez ff709a65e5 Add FROM targets aliasing to avoid conflict in embeds 2018-02-21 09:23:08 -05:00
steve-chavez 8e2a0e05ea Refactor QueryBuilder and DbRequestBuilder
- Refactor ORDER BY and LIMIT query fragments
- Move ReadRequest building to DbRequestBuilder
- Clarify Relation link table attributes
- Change Join Operation to JoinCond
2018-02-21 09:23:08 -05:00
steve-chavez 108f3cd651 Add Preference-Applied header for POST upsert
- Ensure creating nothing on ignore-duplicates succeeds
- Refactor locationF query
2018-02-21 07:33:54 -05:00
steve-chavez 6675821c64 Allow PUT method: UPSERT of a single row 2018-02-21 07:33:54 -05:00
steve-chavez 85b1dc0eb4 Add UPSERT for POST with Prefer:resoultion=merge/ignore-duplicates 2018-02-21 07:33:54 -05:00
Joe Nelson 102392e4ab Move canadaduane's changelog entry to Added 2018-02-19 21:55:03 -06:00
Duane JohnsonandJoe Nelson a46b6f5020 Pass custom settings to the DB's SESSION (#1063)
- allows queries to refer to current_setting('app.settings.foo') to retrieve variables
- useful for 12-factor apps (app data can be in environment)
- provides workaround for AWS Relational Database Service (RDS) not
  allowing `ALTER DATABASE SET 'app.[KEY]' TO '[VALUE]'` on database.
2018-02-19 12:41:22 -06:00
steve-chavez 70ce1b9329 Reduce memory usage of uniform json array restriction 2018-01-27 13:55:20 -05:00
steve-chavez 516976e32f Add memory usage tests 2018-01-10 11:43:16 -05:00
steve-chavez f7e7834a1c Handle overloaded function case
* Add test for params=single-object on GET

* Add tests for procs with DEFAULT args

* Add tests for overloaded functions

* Add test for PATCHing with an empty json array, this previously
  gave a "Something is wrong" error
2018-01-10 11:43:16 -05:00
steve-chavez 38f3bcf4a6 Reduce memory usage by avoiding Aeson encode
It was detected that Aeson encoding had high memory usage when
the json payload was large, around x60 the payload size.
With this change we get around x10 payload size memory usage.

The encodeUtf8(when doing a Text -> ByteString with `toS`) function
on a large payload also contributed to the high memory usage.

Main idea to reduce the memory usage was to let the ByteString coming
from the request body go to the database unchanged.
2018-01-10 11:43:16 -05:00
steve-chavez f159233de8 Change UPDATE query to a parametrized query 2018-01-10 11:43:16 -05:00
steve-chavez 02a286a4b1 Change callProc query to a parametrized query 2018-01-10 11:43:16 -05:00
steve-chavez 85d9feeeab Add set schema to middleware
Fix #828, breaking change: computed columns now only work if they are on
the config schema.

Fix #835, tests now not depend on the search_path of the
postgrest_test_authenticator.
2018-01-10 09:50:20 -05:00
Joe Nelson f9e770b583 Bump to 0.4.4.0 2018-01-08 21:01:39 -06:00
Ruslan TalpaandGitHub effbec234f Faster queries using json_agg 2017-12-12 15:44:50 +02:00
Ruslan TalpaandGitHub e4183780a9 Fix #1016 2017-12-12 15:31:39 +02:00
Joe NelsonandGitHub 804c0b7f6b Include git sha in version (#1022) 2017-12-08 00:29:44 -06:00
steve-chavez fef7d949d9 Fix #411, remove the need for pk in &select for parent embed 2017-11-30 12:56:18 -05:00
steve-chavez be630aa680 Change phrase/plain full text search syntax 2017-11-27 11:38:54 -05:00
Joe Nelson 678b855614 Changelog entry for nextstopsun
Also trim trailing whitespace
2017-11-25 15:15:14 -06:00
Nikolai LebedevandJoe Nelson 546b766022 Reduce join size on allColumns for start speed (#1021) 2017-11-25 15:11:59 -06:00
steve-chavez 57477749aa Run tests on both PostgreSQL 9.4 and 9.6 on CircleCI 2017-10-18 17:41:55 -05:00
steve-chavez 188f947437 Maintain backward compatibility on GUC headers feature
If pg version >= 9.6 is detected the feature is enabled,
also all of the 9.6 dependent tests are moved to their own spec.
2017-10-18 17:41:55 -05:00
steve-chavez b9a591aecb Add ability to map GUC to http response headers 2017-10-18 17:41:55 -05:00
steve-chavez 38de56de4a Add ability to map raise errorcode/message to http status 2017-10-18 17:41:55 -05:00
Pi3randJoe Nelson d9a250d2cb Remove hjsonpointer (#1004)
It is a deps of hjsonchema and does not need to be pinned to a specific version.
2017-10-18 13:11:21 -04:00
Pi3randJoe Nelson 2b5ae34c5a Update jose to 0.6 (#997) 2017-10-15 10:49:25 -04:00
steve-chavez d1a8c3a6f8 Fix #974 RPC error when function has single OUT param 2017-10-09 10:45:17 -05:00
steve-chavez 7a3f350f1c Fix bad OpenAPI output when having functions with OUT/INOUT params 2017-10-09 10:45:17 -05:00
steve-chavez e1cab584a3 Add test for #996 embed bug when table name = column name 2017-10-09 10:45:17 -05:00
grotsevandJoe Nelson 8f49f731d0 Fix embedded column conflicts table name (#996) 2017-10-05 21:03:27 -05:00
Joe NelsonandGitHub 7bf384d0f8 Link to new docker hub repo (#995) 2017-10-03 11:04:51 -05:00
Eric BréchemierandJoe Nelson 65c9d549c1 Read secret file as binary, without premature parsing as UTF-8 (#876) (#988) 2017-09-28 21:54:50 -05:00
Steve ChávezandJoe Nelson a6cce691b5 Fix RPC GET filters bugs with not/{plain,phrase}fts operators (#980) 2017-09-26 20:07:36 -07:00
Elliot MurphyandJoe Nelson 3ccae4bb8b Allow configurable audience claim (#975) 2017-09-26 07:29:20 -07:00
Steve ChávezandJoe Nelson 4ba27d84a4 Fix some OpenAPI issues (#970)
* Fix #933, update externals docs url to current version

* Fix #962, openApi don't err on nonexistent schema

* Fix #954, make OpenAPI rpc output dependent on user privileges
2017-09-25 23:09:52 -07:00
Steve ChávezandJoe Nelson cf19ad0369 Allow GET on RPC (#946) 2017-09-17 11:16:46 -05:00
Joe NelsonandGitHub 32117ba477 Dockerfile has typo in server proxy uri variable (#968)
* s/URL/URI/ in dockerfile

* Treat blank proxy uri as missing
2017-09-09 15:58:37 -05:00
Joe NelsonandGitHub 893b66c969 Merge pull request #964 from steve-chavez/tsquery-options
Allow specifying dictionary and plain/phrase in full text search
2017-09-09 10:17:02 -05:00
steve-chavez 6c2f179b48 Remove isnot and notin operators and refactor 2017-09-08 19:56:00 -05:00
steve-chavez dff4d766a8 Allow dictionary and plain/phrase in fts 2017-09-08 19:56:00 -05:00
steve-chavez d98a05023d Change circle build-test job to use PostgreSQL 9.6.2 2017-09-08 19:56:00 -05:00
ilovezfsandJoe Nelson 0d37be9017 Constrain jose to < 0.6 (#967) 2017-09-08 13:16:00 -05:00
Steve ChávezandJoe Nelson b478e5158b Bump version (#963) 2017-09-06 11:05:14 -05:00
Steve ChávezandJoe Nelson 4901f4327f Add autorelease for linux distros, osx and windows (#923) 2017-09-05 18:37:15 -05:00
daurnimatorandJoe Nelson adb3f5c8c0 Allow passing URI with database to tests (#942) 2017-08-23 19:42:05 -05:00
Joe NelsonandGitHub 9b5ce3a8af Treat blank host config param as missing (#948)
Docker may pass in an empty string, in which case use the default value
2017-08-23 19:41:45 -05:00
Felix YanandJoe Nelson 9af8db0e76 Allow optparse-applicative 0.14 (#947) 2017-08-20 13:14:08 -05:00
Russell DaviesandJoe Nelson 59a320fd44 Add support for range operators (#938) 2017-08-20 09:45:27 -05:00
daurnimatorandJoe Nelson c72bc37630 OpenAPI: Split table comment into summary and description (#931) 2017-08-15 09:52:20 -05:00
Felix YanandJoe Nelson 69070b64f9 Fix compatibility with protolude-0.2 (#939)
This allows the Arch Linux PostgREST package to build cleanly
2017-08-14 01:36:33 -05:00
Joe NelsonandGitHub d89937a6f3 Upgrade to fast new hasql (#836) 2017-08-10 01:54:15 -05:00
Joe NelsonandGitHub 9af539b51c Treat blank pre-request config as missing (#929) 2017-08-09 08:42:32 -05:00
Joe NelsonandGitHub 0ed4215a0d Support asymmetric JWK (#919) 2017-08-09 08:42:00 -05:00
George TrudeauandJoe Nelson 2243f4b653 Improve relations initialization time (#924) (#926) 2017-08-05 14:04:52 -05:00
Ruslan TalpaandGitHub b03e3fbec7 Ability to embed using a specific relation when there are multiple between tables, fixes #907 (#918)
* Ability to embed using a specific relation when there are multiple between tables, fixes #907

* fix lint errors

* fix code comments

* add type comments
2017-07-25 19:01:35 +03:00
Lucas DesgouillesandJoe Nelson 6d5f72bf5f Update OpenAPI (SQL COMMENT to description, constraints, cleaning up) (#885) 2017-07-22 10:56:23 -05:00
Eric BréchemierandJoe Nelson 7fcdbf9153 strip newline before Base64 decoding (#916) 2017-07-20 09:21:31 -05:00
Steve ChávezandJoe Nelson 968bf9ce59 Put traditional Filters inside [LogicTree] (#911) 2017-07-19 18:01:37 -05:00
Peter SomogyvariandJoe Nelson d6c47006b2 fix(doc): server-proxy-uri -- URI, not URL (#914)
There was this typo in the ./docker/postgrest.conf example file, hoping to spare others from having to chase this straight back to the source code.
2017-07-17 08:18:51 -07:00
Pfalzgraf MartinandJoe Nelson c47e37177d comments for Main.hs (#888) 2017-07-11 08:11:20 -07:00
Joe NelsonandGitHub 1513c88f77 Merge pull request #895 from steve-chavez/binary-proc
Binary output support for RPC
2017-07-07 22:34:06 -05:00
steve-chavez cce3138cc1 Add test for a proc name = return col name, Fix #901 2017-07-07 16:32:16 -05:00
Joe NelsonandGitHub 7cc38330a8 Interpolate boolean env vars from conf file (#897) 2017-06-25 23:30:23 -05:00
steve-chavez f0bd507ab9 Add binary output to rpc, Fix #883 2017-06-24 22:45:53 -05:00
steve-chavez ecd58e1ad2 Refactor rpc to not use count when returning scalar 2017-06-24 21:07:07 -05:00
steve-chavez 4cf1bcec64 Add test cases for procs that returns array and setof scalars 2017-06-24 21:07:07 -05:00
Steve ChávezandJoe Nelson b007428142 Allow more than two conditions in a single and/or, Fix #889 (#892) 2017-06-21 00:52:21 -05:00
Joe Nelson 35d5e930ea Bump version 2017-06-11 17:14:19 -05:00
Daniel PfeiferandJoe Nelson 3165d5dbc0 Dockerfile: Clean up to save space (#891)
Running postgrest requires the libpq library runtime, not the -dev
package.

Make sure that tools that are used to download and extract the postgrest
package do not remain installed in the image.
2017-06-11 15:06:58 -05:00
Lucas DesgouillesandJoe Nelson 1c09058628 Fix broken link to 'building from source' in CONTRIBUTING (#886) 2017-06-08 09:10:29 -05:00
Joe NelsonandGitHub 50de0536de Allow csv formatting in rpc output (#882) 2017-05-27 00:07:23 -05:00
Joe NelsonandGitHub 74227d3c2b Use env interpolation in config (#826) 2017-05-24 22:50:09 -05:00
Joe Nelson 3dbcf9cbc3 Heroku deploy button for master branch again 2017-05-24 21:37:22 -05:00
Joe Nelson 44b3bd5fa5 Demo server is down and I don't care to maintain it 2017-05-20 11:53:37 -05:00
Steve PhillipsandJoe Nelson b09b677cd8 README.md: Fixed typo (sentence fragment) 2017-05-16 22:02:57 -05:00
SteveBashandJoe Nelson 8593600cfa Transition IN operator and select query param to new block delimiters 2017-05-13 16:49:25 -05:00
SteveBashandJoe Nelson b8eb2cd9c1 Add and/or params for complex boolean logic
* Add support for and/or params in GET, POST, PATCH and DELETE
* Restrict usage of IN in and/or to be inside parens e.g. in.(1,2,3)
* Allow quoting operators for values that have ',)' chars
  inside and/or e.g. eq."(entity,1)"
2017-05-13 16:49:25 -05:00
SteveBashandJoe Nelson 59abecaf5b Add missing tests for NOT IN accepting empty values and refactor
Go back to operators map since the previously defined
Operator constructors added innecesary complexity also
rename Path to EmbedPath
2017-05-13 16:49:25 -05:00
Steve ChávezandJoe Nelson 3e26c1a83f Add connection retrying on startup and SIGHUP, Fix #742 (#869)
* Add connection retrying on startup and SIGHUP, Fix #742
* Ensure that only one connection worker can run at a time
* Change ConnectionError status code to 503 and add automatic connection retrying
2017-05-06 19:11:18 -05:00
Joe Nelson c912601e1c Merge branch 'build-0.4.1.0' 2017-04-30 00:06:25 -05:00
Joe Nelson 7ba880e902 Re-enable openapi tests on CI 2017-04-29 21:07:42 -05:00
Joe Nelson d99462be01 Revert "Fix swagger-ui when "server-proxy-uri" is not set (#857)"
This was a bug in swagger-ui, not postgrest
2017-04-29 21:02:14 -05:00
Joe Nelson 0b436d49e6 Bump version 2017-04-29 21:02:14 -05:00
Joe NelsonandGitHub f4d84e4b58 Fix deploy to heroku button for v0.4 (#863)
* Bring back the Heroku button!

* New params for updated buildpack
2017-04-19 19:12:58 -05:00
Joe Nelson c8f4173262 Add link to patreon campaign 2017-04-13 17:31:14 -05:00
Steve ChávezandJoe Nelson 0a9d9cdded Fix #701: allow quoted values for IN operator, refactor and Fix #641: allow IN filter to have no values (#854) 2017-04-11 00:57:44 -05:00
Ruslan TalpaandJoe Nelson 5fffbbe381 Make HTTP headers available as GUCs #800 (#849) 2017-04-10 19:23:48 -05:00
Joe NelsonandGitHub 3e7a8b5f85 Use union merge strategy on changelog (#859) 2017-04-08 10:36:02 -05:00
Marios KoulakisandJoe Nelson 8557ee55f5 Update with empty json (#852) 2017-04-07 22:21:50 -05:00
Joe NelsonandGitHub 56bdf59e14 Read only proc tx (#844) 2017-04-07 12:52:16 -05:00
Feynman LiangandJoe Nelson 77af16c9e4 Fix swagger-ui when "server-proxy-uri" is not set (#857) 2017-04-07 08:51:29 -05:00
Feynman LiangandJoe Nelson 0cf49bb8b6 Adds application/json to inspect type (#853) 2017-04-06 10:43:56 -05:00
Ted KornishandJoe Nelson 10f6ce45db Add instructions on running tests to CONTRIBUTING.md (#793) 2017-04-04 22:08:39 -05:00
Marios KoulakisandJoe Nelson 9b4b45671c Accept empty string as empty json body when calling an rpc (#832) 2017-03-27 21:34:03 -07:00
Steve FarmerandJoe Nelson 0b486ccf44 Increase Warp connection timeout (#834) 2017-03-21 19:50:24 -07:00
Steve ChávezandJoe Nelson 47b023e858 Fix proc resource embedding issue with search_path and refactor return type (#831) 2017-03-20 23:50:42 -07:00
Diogo BiazusandJoe Nelson 206ab163b6 Refactor error type and fix nested error message [fix #791] (#829) 2017-03-12 22:02:13 -07:00
Joe NelsonandGitHub 2aabbbae58 Dockerfile that works with postgrest 0.4 (#810)
Also an example docker-compose
2017-02-17 23:43:35 -08:00
Diogo BiazusandJoe Nelson 728e40c96c Update hspec-wai to 0.8.0 (#799) 2017-02-14 21:18:01 -08:00
Steve ChávezandJoe Nelson 98438c437f Proposal for binary output (#802) 2017-02-14 20:36:51 -08:00
Joe NelsonandGitHub 84f68c68cb Public schema table embedding (#803)
* Alter fixture to test objects in public schema

Properly erase and restore the public schema -- requires permissions to install pgcrypto each time.

* Test for fks through public schema tables

* Thanks @fab1an
2017-02-12 16:32:15 -08:00
Pi3randJoe Nelson 1d8318ce26 Increase lower and upper bound for optparse-applicative (#794) 2017-02-03 10:00:13 -08:00
Joe Nelson a61c783e2d Add link to config docs in message 2017-01-22 16:18:41 -08:00
Joe Nelson 69fd5f1b33 HTTP for docs link 2017-01-20 00:39:23 -08:00
Joe NelsonandGitHub 9b407034a8 Bump version 2017-01-20 00:21:08 -08:00
Steve ChávezandJoe Nelson c15f693dbb Move DbRequest building functions to new module (#787) 2017-01-17 21:59:59 -08:00
Steve ChávezandJoe Nelson 7e2cb5fe1c Fix alias in select for mutate queries and small refactor (#779) 2017-01-16 23:20:02 -08:00
Joe NelsonandGitHub 104a7ed4fa Use a mime type to request singular JSON responses (#763) 2017-01-16 15:11:37 -08:00
Ian MackenzieandJoe Nelson e72e4491d1 Fix broken link in README (#782)
At least for me the current link results in a 404 page.
2017-01-06 07:11:07 -08:00
Ruslan TalpaandJoe Nelson 16fd3a57ff Fix #746 (#769)
also fix broken text env

After the last couple of commits the tests ran correctly only on a
fresh db, in addition, the roles within the db were not created/dropped
on each request and we need that since their privileges differ and we
need to to have an absolute clean db on each execution
2016-12-18 15:46:53 -08:00
Trevor BasingerandJoe Nelson e364cbc3ff Support binary (b64) JWT secrets (#772) 2016-12-11 09:01:03 -08:00
Diogo BiazusandJoe Nelson 649a841ef4 Fix embeded relations error message (#767) 2016-12-10 11:59:17 -08:00
Ruslan TalpaandJoe Nelson 15c95399f8 Prevent database memory cosumption by prepared statements caches (#768) 2016-12-09 09:46:28 -08:00
dsimunicandJoe Nelson 0d85152e42 Improve test db management (#747) 2016-12-01 21:06:10 -08:00
Joe NelsonandGitHub 6c009b71d3 Add constraint for vanilla cabal builds (#752) 2016-11-30 19:33:44 -08:00
Joe NelsonandGitHub 0f66e99f78 Merge pull request #745 from diogob/moves_request_error_handling_to_user_api_request
Moves request error handling to user api request
2016-11-30 09:17:56 -08:00
Diogo Biazus 6c275fcec2 Coalesce types UniformObjects and Payload into the new PayloadJSON 2016-11-27 23:55:26 -05:00
Diogo Biazus 16ac034aab Tidy up postgrest function body 2016-11-27 23:55:19 -05:00
Diogo Biazus b158b4924e Use rightToMaybe instead of case 2016-11-27 23:55:14 -05:00
Diogo Biazus 98ada3f9ee Change nesting of if clause making code more symetrical 2016-11-27 23:55:07 -05:00
Diogo Biazus 090a62a2c8 Removes error case from Payload type. Now we don't build a payload when the parsing fails. 2016-11-27 23:55:02 -05:00
Diogo Biazus 654ac6e62e Move function to conver ApiRequestError to a Http Response to the Error module. 2016-11-27 23:54:58 -05:00
Diogo Biazus fec769c80e Move range validation to userApiRequest. 2016-11-27 23:54:52 -05:00
Diogo Biazus 6a02d9efd5 Remove PayloadParseError matcher from main app case since now we validate the request before building an ApiRequest. 2016-11-27 23:54:48 -05:00
Diogo Biazus b8ddc30252 Remove ActionInappropriate constructor from type Action since now we validate the request before building an ApiRequest. 2016-11-27 23:54:37 -05:00
Diogo Biazus 47c4fbc8ef Introduce the type ApiRequestError and make userApiRequest return an Either ApiRequestError ApiRequest 2016-11-27 23:54:23 -05:00
dsimunicandJoe Nelson ae40641963 Introduce new 'Prefer' header, params=single-object (#739) 2016-11-20 14:14:39 -08:00
Joe NelsonandGitHub 4df853eff9 Take config path as first param, not flag
Include example config in usage message
2016-11-17 22:45:59 -08:00
Diogo BiazusandJoe Nelson 41c1cf6e01 Reduce app function (#733)
Simplify the big app function
2016-11-10 20:42:00 -08:00
Joe NelsonandGitHub c3822da2d2 Merge pull request #728 from steve-chavez/master
Fix fatal error on postgres unsupported version, also fix #577 format…
2016-10-31 23:06:02 -07:00
Joe NelsonandGitHub 0fa072d04c Merge pull request #726 from phanimahesh/nix-integration
add nix.packages to stack.yaml
2016-10-30 22:21:13 -07:00
Joe NelsonandGitHub 6dba37be47 Merge pull request #729 from jhr007/master
Update Contributing.md "Build from source link"
2016-10-29 22:57:32 -07:00
jhr007andGitHub fce84397f4 Update "Build from source link" 2016-10-29 18:46:39 -05:00
SteveBash 4064a7b984 Fix fatal error on postgres unsupported version, also fix #577 format of supported version 2016-10-29 13:38:19 -05:00
Joe NelsonandGitHub d78ef56314 Merge pull request #725 from begriffs/count-star
count(*) is faster than count(1)
2016-10-26 22:09:43 -07:00
J Phani Mahesh 37d3c851cf add nix.packages to stack.yaml
postgrest depends on postgres and zlib. When using stack's nix
integration, these dependencies must be specified explicitly.

Note that `--nix` must be passed to stack, or `nix.enable:true` must
be set in `stack.yaml` to enable using nix while building.
2016-10-26 15:49:53 +05:30
Joe NelsonandGitHub fbf345fc68 Fix hyphenation 2016-10-24 10:07:28 -07:00
Joe Nelson 9dddd144a4 count(*) is faster than count(1) 2016-10-23 20:44:42 -07:00
Joe NelsonandGitHub 09b63eafad Update readme (#723) 2016-10-23 20:24:22 -07:00
Joe NelsonandGitHub 9233d90075 Read settings from config file (#714) 2016-10-20 08:34:57 -07:00
Joe NelsonandGitHub 74e68408e6 Bring packages up to date (#722)
* Bring packages up to date

* New LTS includes deps now

Use stack to select most versions

* Alphabetize packages

* Strip down packages

Switch tests to protolude

* Appease cabal check with base version constraint
2016-10-19 22:25:58 -07:00
Joe NelsonandGitHub 726b2b9d18 Merge pull request #721 from begriffs/prune-repo
Remove things to lighten support burden
2016-10-19 20:44:34 -07:00
Joe NelsonandGitHub 0c0396d0d4 Merge pull request #720 from begriffs/claim-sql-name
Set request.jwt.claim.*
2016-10-18 08:24:18 -07:00
Joe Nelson 556b7129ca Remove things to lighten support burden 2016-10-17 22:41:38 -07:00
Joe Nelson c6cd8145eb Set request.jwt.claim.* 2016-10-17 22:32:36 -07:00
Joe Nelson 5d904dfd66 Merge PR #713
Resolve petty CHANGELOG conflict
2016-10-12 20:52:40 -07:00
Joe NelsonandGitHub dcb3b6ed4d Merge pull request #712 from ruslantalpa/merge_upstream
fix relation detection bug and allow offset on lower levels
2016-10-11 23:32:46 -07:00
SteveBash 191601f129 Add ability to order by jsonb keys, fix #644 2016-10-05 14:46:12 -05:00
Ruslan Talpa 211e3d4141 fix relation detection bug and allow offset on lower levels 2016-10-05 11:34:07 +03:00
Joe NelsonandGitHub 980680f3d6 Merge pull request #711 from steve-chavez/master
Allow using nulls order without explicit order direction, fix #552
2016-10-04 20:00:34 -07:00
SteveBash 50ae48295d Allow using nulls order without explicit order direction, fix #552 2016-10-04 16:11:19 -05:00
Joe NelsonandGitHub 9b7685e5d1 Merge pull request #703 from begriffs/jwt
Big bag of JWT fixes for accumulated issues
2016-10-03 10:40:06 -07:00
Joe Nelson d17cfb0c5d Note these changes in CHANGELOG 2016-10-03 10:15:19 -07:00
Joe Nelson 9cd65a4033 Avoid calling getPOSIXTime more than necessary 2016-10-03 09:55:28 -07:00
Joe Nelson 5a166e8e80 Fix custom pre-request handler
Uses Ruslan's technique
2016-09-27 23:17:08 -07:00
Joe Nelson 06363ccc77 WIP: add argument for custom pre-request handler 2016-09-27 00:19:05 -07:00
Joe Nelson 2f8ac24128 Do not share JWT secret with SQL 2016-09-24 23:40:54 -07:00
Joe Nelson 71bc666a8e Forgot jwt fixture, also hlint fix 2016-09-24 21:32:47 -07:00
Joe Nelson 12a8c682bd Use sql to generate jwt, rather than custom haskell 2016-09-24 21:32:47 -07:00
Joe Nelson 62ed9e2c4d Do not require jwt secret, but die on auth without it 2016-09-24 21:28:08 -07:00
Joe Nelson 449480bf01 Require JWT secret, remove default, optionally read from file
Fixes #474, fixes #495 when file is used
2016-09-24 21:28:08 -07:00
Joe Nelson 7bf5b0106d Semantic WWW-Authenticate headers for problematic JWT
Adapting an OAuth 2.0 convention from RFC 6750 for use with JWT
2016-09-24 21:28:08 -07:00
Joe Nelson fb5fce026d Issue http 401 for expired jwt
Fixes #512
2016-09-24 21:28:08 -07:00
Joe NelsonandGitHub 35da4809d4 Merge pull request #706 from begriffs/func-descriptions
Include RPC endpoints in OpenAPI output
2016-09-24 20:38:42 -07:00
Joe Nelson a88a704bef Parse proc arg definitions that have multi-word types 2016-09-22 23:30:47 -07:00
Joe Nelson 539df21627 WIP: more comprehensive test of rpc args
Breaks toks2arg
2016-09-21 23:54:08 -07:00
Joe Nelson 0314f4bdea First test of OpenAPI RPC params 2016-09-21 20:34:03 -07:00
Joe Nelson ffd2859cba Changelog 2016-09-11 16:23:41 -07:00
Joe Nelson 362ad7b7d0 Hlint adjustment 2016-09-11 16:22:32 -07:00
Joe Nelson 25c2cd1f2d Note which rpc params are optional 2016-09-11 15:15:38 -07:00
Joe Nelson 466090c79b Expose stored proc args properly as json body param 2016-09-11 12:33:19 -07:00
Joe Nelson b9777dec35 Include proc parama and their types in openapi 2016-09-08 23:00:52 -07:00
Joe Nelson f49c6aa0f3 WIP: show procs in OpenAPI description
Thanks @LogvinovLeon
2016-09-08 21:23:16 -07:00
Joe NelsonandGitHub f3b79bcc40 Use table alias unlikely to conflict with a column name (#705) 2016-09-08 08:16:47 -07:00
Joe NelsonandGitHub af75988dd4 No count by default (#700)
* WIP: disable counting total table size by default

* Remove commented tests since PUT is no longer supported

* Fix tests for invalid range which require count

* Do not report count in PATCH response if not asked

* Return count of deleted items only if asked

* Return count for bulk insert when requested

* Changelog entry
2016-09-04 10:31:24 -07:00
Joe NelsonandGitHub 7278507c42 Use comma rather than semicolon to split header values (#698) 2016-08-21 15:26:00 -07:00
Diogo BiazusandJoe Nelson 6f737056a2 Protolude completion in library and executable (#697) 2016-08-21 15:10:19 -07:00
Ben GamariandJoe Nelson 298753d59e App: Remove erroneous import of Data.Monoid (#687) 2016-08-19 16:15:41 -07:00
Joe NelsonandGitHub 1d5a0e4316 Per-route content negotiation, and OpenAPI only for root (#693)
* WIP: remove non-openapi root spec

* Refactor ContentType

Different endpoints will favor one type over another

* Permit different Accept headers per endpoint

* Lint

* Add charset to Content-Type only when used as a header

Keep it out of error messages

* Accept: */* is last resort, not first

* Changelog

* makeMimeList consistently

* Remove schema description from OPTIONS response
2016-08-19 16:07:22 -07:00
Erlend HambergandJoe Nelson 35c5b190b4 Singular response for PATCH request (#634)
* Allow a singular entity to be returned from an update

Since a `PATCH` will be an update that could affect many rows, there might be
more than one object returned. By allowing asking for a singular response,
the/an object will be returned instead of a list. This is useful in cases when
the `PATCH` is against single entity (i.e. `/items?pkey=eq.99`).

* Add tests for plurality=singular for `PATCH` requests

* Disallow updating more than one row if `plurality=singular`

As discussed in #634, we don't want to allow updating several rows with
a `PATCH` request when the `Prefer` header specifies
`return=representation;plurality=singular` as this would almost
certainly be a client error.

* Add test for patching multiple objects with singular response

Patching > 1 object with `return=representation;plurality=singular`
should return `400 Bad Request`.

* Only add singleton range for read API requests

* Disallow inserting more than one row if `plurality=singular`

Disallow inserting several rows with a `POST` request when the `Prefer`
header specifies `return=representation;plurality=singular` as this
would almost certainly be a client error.

* Only import `q` from `Text.InterpolatedString.Perl6`

* Update OpenAPI response for PATCH/POST to mention `plurality=singular`

The OpenAPI response for PATCH/POST requests now includes the `Prefer` value
`return=representation;plurality=singular`.

* Add entry in changelog
2016-08-19 14:02:14 -07:00
Diogo BiazusandJoe Nelson df6cbc4afa Protolude second movement (#677)
* Replace Prelude import for Protolude in middleware

* Remove qualifier from Text type

* Replace Prelude for Protolude, replace 'cs' for 'toS' calls, also change some name bindings

* Replace Prelude in main for Protolude. Replace error calls for panic calls. Also replace cs for toS

* Replace Prelude for Protolude

* Replace Prelude for Protolude in OpenAPI
2016-07-23 12:20:36 -07:00
Joe NelsonandGitHub d93d07795b Merge pull request #675 from begriffs/bump-hjsonschema
Use real version of hjsonschema
2016-07-18 12:19:22 -07:00
Joe Nelson c98450d1d5 Use real version of hjsonschema
Its raw commit on github no longer exists
2016-07-18 11:37:38 -07:00
Joe NelsonandGitHub b6e100096d Merge pull request #672 from begriffs/cache-warning
Add schema cache freshness warning
2016-07-16 17:04:15 -07:00
Joe Nelson 338c4de8b4 Add schema cache freshness warning 2016-07-11 16:46:39 -07:00
Joe NelsonandGitHub b5d720e091 Merge pull request #649 from hudayou/proxy-awareness
Add proxy awareness for OpenAPI spec generation
2016-07-07 08:25:30 -07:00
Joe NelsonandGitHub 2b1ca9f5b7 Merge pull request #662 from begriffs/raise-400
Translate raise_exception to http 400
2016-07-07 08:15:26 -07:00
Jacky Hu 1fef1991ef Add proxy awareness for OpenAPI spec generation
Proxy uri from command line options takes precedence over host and port
2016-07-07 19:20:40 +08:00
Joe Nelson e4cf1ce207 Translate raise_exception to http 400
Fixes #661
2016-07-06 21:48:32 -07:00
Joe NelsonandGitHub 63046baffd Merge pull request #620 from ruslantalpa/rpc_refactor
Rpc refactor
2016-07-06 20:51:05 -07:00
Joe NelsonandGitHub d102d954ac Merge pull request #657 from diogob/protolude-first-fugues
Protolude first fugues
2016-06-30 08:27:58 -07:00
Diogo Biazus 578ae6b5dd Import GHC.Show without qualifying it 2016-06-30 08:35:42 -04:00
Joe NelsonandGitHub f7aa8b7ad5 Merge pull request #656 from diogob/simplify-pTreePath
Remove unnecessary string conversions
2016-06-29 20:37:15 -07:00
Diogo Biazus 7b9eb86fd0 Stop running openpi test until we figure out a way to run it in circle without a suite timeout 2016-06-29 10:14:38 -04:00
Diogo Biazus b17fa09393 Add explicit import Prelude to remaining modules to compile and run specs 2016-06-29 09:26:47 -04:00
Diogo Biazus 0e172b8030 Port Auth to prolude 2016-06-29 09:25:57 -04:00
Diogo Biazus 3308012dcf Add protolude to dependencies, include NoImplicitPrelude in default extensions and port Error module to protolude. 2016-06-29 09:25:57 -04:00
Diogo Biazus 376b67be9e Remove unnecessary string conversions 2016-06-29 00:18:29 -04:00
Ruslan Talpa 72aa664b61 CHANGELOG 2016-06-28 12:20:53 +03:00
Ruslan Talpa 1372de6f43 query simplification by @diogob 2016-06-28 12:18:12 +03:00
Ruslan Talpa 430baf23b9 embed entities to the results returned by procs 2016-06-28 12:09:58 +03:00
Ruslan Talpa 2f166088c8 response shaping and filtering for rpc proc calls 2016-06-28 12:09:58 +03:00
Ruslan Talpa 744bbf7203 cache the names of functions returning JWT and avoid extra query on each rpc call 2016-06-28 12:09:58 +03:00
Ruslan Talpa a253ff325d don't check if proc exists, just call it and return 400 if it does not 2016-06-28 12:09:57 +03:00
Joe NelsonandGitHub 67668a02c2 Merge pull request #653 from bgamari/master
Add version bounds on http-types
2016-06-26 09:47:12 -07:00
Ben Gamari cd27e9dcde Add version bounds on http-types
Postgresst uses hAllow, which was only added in 0.9.
2016-06-26 06:02:30 -04:00
Joe NelsonandGitHub da97bb84e3 Merge pull request #651 from begriffs/upgrade-hasql-transaction
Upgrade hasql-transaction
2016-06-25 21:49:57 -07:00
Joe Nelson 0bf4146633 Speed up build time on CI 2016-06-25 21:35:40 -07:00
Joe Nelson 9c42a78cf3 Upgrade hasql-transaction 2016-06-25 21:24:21 -07:00
Joe NelsonandGitHub 9106f70cfa Merge pull request #650 from begriffs/split-docs
Move docs out of this repo
2016-06-24 14:54:16 -07:00
Joe Nelson 99d9984111 Move docs out of this repo
They live in a separate repo now
2016-06-24 13:05:27 -07:00
Joe NelsonandGitHub acd75a3997 Merge pull request #639 from hudayou/swagger2
Provide a swagger2 spec for the dynamic API
2016-06-20 15:12:58 -07:00
Martin GammelsæterandJoe Nelson 46b3ce5631 Name custom types and domains on OPTIONS requests (#640) 2016-06-20 09:49:52 -07:00
Jacky Hu e571cb9fcb Ability to set addresses to listen on
Fixes #461
2016-06-18 10:46:02 +08:00
Jacky Hu c1764c4976 Update changelog 2016-06-18 10:18:43 +08:00
Jacky Hu 86e81c135e Use hjsonschema from the develop branch 2016-06-18 10:18:43 +08:00
Jacky Hu 1a25bb501f Add 415 test for openapi req on none root path 2016-06-18 10:18:43 +08:00
Jacky Hu a9ffde4d5f Fix the invalid order parameter bug found by test 2016-06-18 10:18:43 +08:00
Jacky Hu 0220040341 Add test to validate openapi spec 2016-06-18 10:18:43 +08:00
Jacky Hu 11aeb4fbda Add all supported successful responses 2016-06-18 10:18:43 +08:00
Jacky Hu d3d1fbe7e5 Return HTTP 415 on non root path for openapi req 2016-06-18 10:18:43 +08:00
Jacky Hu 816d577f53 Add undocumented "return=none" preference 2016-06-18 10:18:43 +08:00
Jacky Hu a7316aff01 Add a changelog entry 2016-06-18 10:18:43 +08:00
Jacky Hu e88a0577e5 Avoid repeating mime types 2016-06-18 10:18:43 +08:00
Jacky Hu c10bde8d65 Add a simple test for openapi 2016-06-18 10:18:43 +08:00
Jacky Hu 0be5f5299f Reponse with the correct header for openapi 2016-06-18 10:18:43 +08:00
Jacky Hu fd0b354cd0 Remove redundant consumes property for get operation 2016-06-18 10:18:43 +08:00
Jacky Hu 733b2cc05b Move around encodeFn and realted helpers 2016-06-18 10:18:43 +08:00
Jacky Hu 077bb5a434 Verbose parse error for OpenAPI 2016-06-18 10:18:43 +08:00
Jacky Hu d1ed884a8d Introduce the OpenAPI header
Also bring back the original behavior of GET "/"
2016-06-18 10:18:43 +08:00
Jacky Hu 103fa0550c Use a specific name for OpenAPI module 2016-06-18 10:18:43 +08:00
Jacky Hu 563c5fa778 Make hostname configurable 2016-06-18 10:18:43 +08:00
Jacky Hu 0e456543bf Add a root path item 2016-06-18 10:18:43 +08:00
Jacky Hu d34056b3b9 Work around hlint pattern error
See also: https://github.com/ndmitchell/hlint/issues/216
2016-06-18 10:18:43 +08:00
Jacky Hu 74ea4aba37 Fix all hlint errors except for pattern error 2016-06-18 10:18:43 +08:00
Jacky Hu 4b0c5cb36f Disable schema test 2016-06-18 10:18:43 +08:00
Jacky Hu cf4e157de7 Provide a swagger2 spec for the dynamic API
Related issue: #144
2016-06-18 10:18:43 +08:00
Ruslan TalpaandJoe Nelson b916ed907b fix #637 (#647) 2016-06-17 10:45:25 -07:00
Joe Nelson e8426671c0 v0.3.2.0 2016-06-10 22:43:58 -07:00
Joe NelsonandGitHub 455f086880 Remove unix dependency for tests (#636) 2016-06-10 22:35:29 -07:00
Joe NelsonandGitHub 42110643a3 Use newer deps for GHC 8 compatibility (#619) 2016-06-09 18:50:13 -07:00
Joe Nelson e315dbc91e Include allow header in options response (#628) 2016-06-08 23:12:29 -07:00
Joe Nelson e272c2ed08 Merge pull request #626 from edofic/update-operator-docs
Update operator documentation
2016-06-03 23:26:09 -07:00
Andraz Bajt a875db2b82 Update operator documentation 2016-06-03 08:59:39 +02:00
Joe Nelson 02c6de4144 Merge pull request #618 from begriffs/post-empty-obj
Use table defaults for empty object insert
2016-06-02 08:46:47 -07:00
Joe Nelson 7563b5e2f4 Move unwords higher for branch parity 2016-06-02 08:40:24 -07:00
Joe Nelson 5e3d9442af Use table defaults for empty object insert
Fixes #616
2016-06-02 08:40:24 -07:00
Joe Nelson c0c1a260ba Merge pull request #625 from ruslantalpa/return_data_on_delete
Implement select/return representation for DELETE queries (fix #518)
2016-06-02 08:38:46 -07:00
Ruslan Talpa 6ebd7fd2d7 implement select/return representation for DELETE queries (fix #518) 2016-06-02 13:37:33 +03:00
Joe Nelson 24dd4e8626 Merge pull request #608 from ruslantalpa/multilevel_limit
Limit embeded items
2016-05-31 07:57:16 -07:00
Ruslan Talpa dc727f900d suggested cleaup by @begriffs 2016-05-31 14:55:31 +03:00
Michal ŠkopandJoe Nelson 0847a38691 adding check for verified flag before login into Users example 2016-05-27 16:53:46 -07:00
Ruslan Talpa 7c83edc402 Limit embeded items 2016-05-26 09:56:28 +03:00
Joe Nelson e76de196e0 Merge pull request #604 from begriffs/less-frequent-gc
Run GC every 2s rather than 0.3s
2016-05-25 23:29:20 -07:00
Joe Nelson b7331135a6 Merge pull request #607 from league/urlencode-location
Simplify serialization of location header
2016-05-25 20:53:41 -07:00
Christopher League 0940b2dccf Simplify serialization of location header
Possible after bug fix in a Hasql that we picked up with the new
dependency bounds in #606. Also includes test to ensure location header
is omitted on bulk insert.
2016-05-22 21:54:26 -04:00
Joe Nelson 4f53aef74f Merge pull request #606 from begriffs/newdeps 2016-05-22 11:36:15 -07:00
Joe Nelson f4027cb5fd Pin hasql-* dep versions
This allows packdeps to warn us when they get out of date
2016-05-22 11:02:37 -07:00
Joe Nelson 38afe71ec7 Upgrade extra-deps and LTS 2016-05-22 11:00:37 -07:00
Joe Nelson 308c006a30 Quote with-rtsopts correctly 2016-05-21 12:18:10 -07:00
Joe Nelson c7d863c998 Merge pull request #605 from diogob/microlens
Replace lens dependency for microlens
2016-05-21 11:48:07 -07:00
Joe Nelson 44cdc97d71 Run GC every 2s rather than 0.3s
Fixes #565
2016-05-21 11:21:42 -07:00
Joe Nelson a87dcd5553 Merge pull request #603 from diogob/refactor-jwtClaims
jwtClaims should always return Left for invalid JWT
2016-05-21 11:14:57 -07:00
Joe Nelson 592dd39222 Merge pull request #602 from ruslantalpa/order_limit_embeded
Ability to order embedded items (closes #509)
2016-05-21 11:08:24 -07:00
Ruslan Talpa 45d0f85b0d ability to order embeded items (closes #509) 2016-05-21 20:52:13 +03:00
Diogo Biazus b68fcd2522 Replace lens dependency for microlens 2016-05-21 13:31:39 -04:00
Diogo Biazus abd81c998b jwtClaims should always return Left for invalid JWT 2016-05-21 13:18:45 -04:00
Joe Nelson 6a2edb2844 Merge pull request #595 from league/master
URL-encode Location header in 201 response (#588)
2016-05-21 09:52:09 -07:00
Christopher League 5c38b4328b URL-encode Location header (closes #588)
The database returns an array of key-value strings like `"k1=eq.hello
world"`. Haskell URL-encodes the portion after the equal sign and joins
them with `&`.

Includes updates to tests in Feature.InsertSpec: The CompoundPK has been
modified to have one Int and one String. We attempt to add a key with a
String that has spaces and other special characters. This requires that
the returned Location header is properly URL-encoded.
2016-05-20 10:42:55 -04:00
Joe Nelson 2cb04c1d5c Merge pull request #597 from league/avoid-recompile
Tweak .cabal to avoid unneeded recompilation
2016-05-18 09:05:13 -07:00
Christopher League b089e0a7dd Tweak .cabal to avoid unneeded recompilation
Previously when making a change and running `stack test`, it would
compile each module 3 times: for the library, the executable, and the
test suite.

This change more cleanly segregates the hs-source-dirs for each target,
which avoids recompilation. The only source change is moving Main.hs
into its own directory (but it's otherwise unchanged). See also:
<http://stackoverflow.com/questions/6711151/how-to-avoid-recompiling-in-this-cabal-file>
2016-05-18 09:55:52 -04:00
Joe Nelson a21464ddca Merge pull request #592 from begriffs/form-urlencoded
Accept POST requests from HTML forms
2016-05-18 00:38:42 -07:00
Joe Nelson 900b9f1991 Explain use of Left value 2016-05-18 00:06:16 -07:00
Joe Nelson cf16f90fab Merge pull request #590 from begriffs/proper-403
Return proper 401/403 when access denied
2016-05-17 22:56:55 -07:00
Joe Nelson 36a6b10d0d Merge pull request #594 from ruslantalpa/multiple_fks
Fix include entities from the same parent table using two different foreign keys
2016-05-17 22:54:12 -07:00
Douglas CuthbertsonandJoe Nelson 2ac3ad9e37 Fix Windows build issue 589 (#593) 2016-05-17 22:47:39 -07:00
Ruslan Talpa 9e6542680b Fix include entities from the same parent table using two different foreign keys 2016-05-16 15:36:18 +03:00
Joe Nelson 7e41b620ff Accept POST requests from HTML forms 2016-05-15 20:55:07 -07:00
Joe Nelson 18e3c30ad8 Return proper 401/403 when access denied
Fixes #584
2016-05-15 00:56:47 -07:00
Joe Nelson 0dbd0ece9a Merge pull request #586 from ruslantalpa/rename_order_limit_feature
Ability to rename columns/nodes in the output and support "-" in column names
2016-05-15 00:55:12 -07:00
Ruslan Talpa c13f0a369b Support node/column renaming #310 2016-05-12 10:22:19 +03:00
Ruslan Talpa cacc725e41 support dash in column names fix #462 2016-05-11 10:43:34 +03:00
opensrckenandJoe Nelson 0dc33dbf9f fix row level security readme per https://github.com/begriffs/postgre… (#579)
* fix row level security readme per https://github.com/begriffs/postgrest/issues/554

* handle anonymous access to posts / comments tables

* address insertion use case in row-level security readme
2016-05-08 09:31:28 -07:00
Joe Nelson d9205bd838 Do not include Content-Type header for empty body (#580)
* Do not include Content-Type header for empty body

Fixes #544

* Fix lint

* Changelog
2016-05-03 21:18:23 -07:00
Joe Nelson 88aad4b1b6 Reload schema definition on SIGHUP (#570) 2016-04-26 07:50:23 -07:00
Joe Nelson 5aadfba84b Use read-only transaction mode for read requests (#561)
* Make middleware use ApiRequest rather than Request

* Fix outdated comments

* Use read-only transaction mode for read requests

This allows API requests against read replicas
2016-04-15 12:26:40 -07:00
Joe Nelson eae5857d0e Set role only once, and set it before other GUC vars (#560)
* Set role only once, and set it before other GUC vars

Fixes #559

* Unify role/claim logic in claimsToSQL

Suggested by @diogob
2016-04-15 07:30:36 -07:00
Joe Nelson c32d13c8f1 Avoid slow PL/pgSQL exception handling in example (#543) 2016-04-10 15:36:32 -07:00
Joe Nelson 0401a8eb13 Add Docker Hub badge 2016-04-09 15:34:46 -07:00
Joe Nelson 9a1a87ff8e Merge pull request #538 from jpierre03/patch-1
Update postgrest version to 0.3.1.1 in Dockerfile
2016-03-29 18:17:28 -07:00
Jean-Pierre PRUNARET 16e3b16081 Update postgrest version to 0.3.1.1 2016-03-29 22:54:22 +02:00
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
111 changed files with 13683 additions and 2663 deletions
+327
View File
@@ -0,0 +1,327 @@
version: 2
build-distro-bin: &build-distro-bin
machine: true
steps:
- checkout
# cannot interpolate env var and use as a cache key so just copy the Dockerfile to another filename
- run: cp docker/distro_release/Dockerfile.$CIRCLE_JOB docker/distro_release/Dockerfile
- restore_cache:
keys:
- v1-{{ .Environment.CIRCLE_JOB }}-image-{{ checksum "docker/distro_release/Dockerfile" }}
- restore_cache:
keys:
- v1-{{ .Environment.CIRCLE_JOB }}-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run:
name: load or build docker image
command: |
if [[ -e ~/image.tar ]]; then
docker load -i ~/image.tar
else
docker build --rm=false -t $CIRCLE_JOB -f docker/distro_release/Dockerfile.$CIRCLE_JOB docker/distro_release/
docker save $CIRCLE_JOB > ~/image.tar
fi
- run:
name: build binary
command: |
docker run -it \
-v $HOME/.stack:/root/.stack \
-v $(pwd):/source \
-v $HOME/bin/:/root/.local/bin/ \
$CIRCLE_JOB build --allow-different-user --install-ghc --copy-bins
# volumes owned by root if chown is not done the save_cache step fails silently
sudo chown -R circleci:circleci ~/.stack .stack-work
- run:
name: compress binary
command: |
mkdir -p /tmp/workspace/bin
cd /tmp/workspace/bin
tar cvJf postgrest-$CIRCLE_TAG-$CIRCLE_JOB.tar.xz -C ~/bin postgrest
- persist_to_workspace:
root: /tmp/workspace
paths:
- bin/*
- save_cache:
paths:
- ~/image.tar
key: v1-{{ .Environment.CIRCLE_JOB }}-image-{{ checksum "docker/distro_release/Dockerfile" }}
- save_cache:
paths:
- "~/.stack"
- ".stack-work"
key: v1-{{ .Environment.CIRCLE_JOB }}-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
jobs:
build-test-9.4:
docker:
- image: circleci/buildpack-deps:trusty
environment:
- PGHOST=localhost
- image: circleci/postgres:9.4.14
environment:
- POSTGRES_USER=circleci
- POSTGRES_DB=circleci
steps:
- checkout
- restore_cache:
keys:
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run:
name: install stack & dependencies
command: |
curl -L https://github.com/commercialhaskell/stack/releases/download/v1.1.2/stack-1.1.2-linux-x86_64.tar.gz | tar zx -C /tmp
sudo mv /tmp/stack-1.1.2-linux-x86_64/stack /usr/bin
sudo apt-get update
sudo apt-get install -y libgmp-dev
sudo apt-get install -y --only-upgrade binutils
sudo apt-get install -y postgresql-client
stack setup
rm -rf $(stack path --dist-dir) $(stack path --local-install-root)
stack install hlint packdeps cabal-install
- run:
name: build src and tests
command: |
stack build --fast -j1
stack build --fast --test --no-run-tests
- run:
name: run tests
command: |
POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
test/io-tests.sh
- run:
name: run linter
command: git ls-files | grep '\.l\?hs$' | xargs stack exec -- hlint -X QuasiQuotes -X NoPatternSynonyms "$@"
- run:
name: extra checks
command: |
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
- save_cache:
paths:
- "~/.stack"
- ".stack-work"
key: v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
build-test-9.6:
docker:
- image: circleci/buildpack-deps:trusty
environment:
- PGHOST=localhost
- image: circleci/postgres:9.6.2
environment:
- POSTGRES_USER=circleci
- POSTGRES_DB=circleci
steps:
- checkout
- restore_cache:
keys:
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run:
name: install stack & dependencies
command: |
curl -L https://github.com/commercialhaskell/stack/releases/download/v1.1.2/stack-1.1.2-linux-x86_64.tar.gz | tar zx -C /tmp
sudo mv /tmp/stack-1.1.2-linux-x86_64/stack /usr/bin
sudo apt-get update
sudo apt-get install -y libgmp-dev
sudo apt-get install -y postgresql-client
stack setup
- run:
name: build src and tests
command: |
stack build --fast -j1
stack build --fast --test --no-run-tests
- run:
name: run tests
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
build-test-10:
docker:
- image: circleci/buildpack-deps:trusty
environment:
- PGHOST=localhost
- image: circleci/postgres:10.5
environment:
- POSTGRES_USER=circleci
- POSTGRES_DB=circleci
steps:
- checkout
- restore_cache:
keys:
- v1-stack-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run:
name: install stack & dependencies
command: |
curl -L https://github.com/commercialhaskell/stack/releases/download/v1.1.2/stack-1.1.2-linux-x86_64.tar.gz | tar zx -C /tmp
sudo mv /tmp/stack-1.1.2-linux-x86_64/stack /usr/bin
sudo apt-get update
sudo apt-get install -y libgmp-dev
sudo apt-get install -y postgresql-client
stack setup
- run:
name: build src and tests
command: |
stack build --fast -j1
stack build --fast --test --no-run-tests
- run:
name: run tests
command: POSTGREST_TEST_CONNECTION=$(test/create_test_db "postgres://circleci@localhost" postgrest_test) stack test
build-prof-test:
docker:
- image: circleci/buildpack-deps:trusty
environment:
- PGHOST=localhost
- TERM=xterm
- image: circleci/postgres:9.6.2
environment:
- POSTGRES_USER=circleci
- POSTGRES_DB=circleci
steps:
- checkout
- restore_cache:
keys:
- v1-stack-prof-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
- run:
name: install stack & dependencies
command: |
curl -L https://github.com/commercialhaskell/stack/releases/download/v1.1.2/stack-1.1.2-linux-x86_64.tar.gz | tar zx -C /tmp
sudo mv /tmp/stack-1.1.2-linux-x86_64/stack /usr/bin
sudo apt-get update
sudo apt-get install -y libgmp-dev
sudo apt-get install -y postgresql-client
stack setup
- run:
name: build with profiling enabled
command: |
stack build --profile -j1
- run:
name: run memory usage tests
command: |
test/create_test_db "postgres://circleci@localhost" postgrest_test
psql "postgres:///postgrest_test" -f test/fixtures/database.sql
psql "postgres:///postgrest_test" -f test/fixtures/roles.sql
psql "postgres:///postgrest_test" -f test/fixtures/schema.sql
psql "postgres:///postgrest_test" -f test/fixtures/jwt.sql
psql "postgres:///postgrest_test" -f test/fixtures/privileges.sql
test/memory-tests.sh
- save_cache:
paths:
- "~/.stack"
- ".stack-work"
key: v1-stack-prof-dependencies-{{ checksum "postgrest.cabal" }}-{{ checksum "stack.yaml" }}
centos6:
<<: *build-distro-bin
centos7:
<<: *build-distro-bin
ubuntu:
<<: *build-distro-bin
ubuntui386:
<<: *build-distro-bin
release:
docker:
- image: circleci/golang:1.8
steps:
- attach_workspace:
at: /tmp/workspace
- checkout
- run:
name: add body and tars to github release
command: |
go get -u github.com/tcnksm/ghr
START=$(echo $CIRCLE_TAG | cut -c2-)
END='## \['
BODY=$(sed -n "1,/$START/d;/$END/q;p" CHANGELOG.md)
ghr -t $GITHUB_TOKEN -u $CIRCLE_PROJECT_USERNAME -r $CIRCLE_PROJECT_REPONAME -b "$BODY" --replace $CIRCLE_TAG /tmp/workspace/bin
- setup_remote_docker
- run:
name: publish docker image
command: |
docker build --build-arg POSTGREST_VERSION=$CIRCLE_TAG -t postgrest ./docker/
docker login -u $DOCKER_USER -p $DOCKER_PASS
docker tag postgrest postgrest/postgrest:$CIRCLE_TAG
docker push postgrest/postgrest:$CIRCLE_TAG
docker tag postgrest postgrest/postgrest:latest
docker push postgrest/postgrest:latest
workflows:
version: 2
build-test-release:
jobs:
- build-test-9.4:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- build-test-9.6:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- build-test-10:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- build-prof-test:
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
- centos6:
requires:
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/
- centos7:
requires:
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/
- ubuntu:
requires:
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/
- ubuntui386:
requires:
- build-test-9.4
- build-test-9.6
- build-test-10
- build-prof-test
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
branches:
ignore: /.*/
- release:
requires:
- centos6
- centos7
- ubuntu
- ubuntui386
filters:
tags:
only: /v[0-9]+(\.[0-9]+)*/
+1
View File
@@ -0,0 +1 @@
/CHANGELOG.md merge=union
+12 -9
View File
@@ -27,6 +27,14 @@ your contributions.
* 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).
* If your database schema has changed while the PostgREST server is running,
send the server a `SIGHUP` signal or restart it to ensure the schema cache
is not stale. This sometimes fixes apparent bugs.
## Code
### Haskell Conventions
@@ -40,16 +48,11 @@ your contributions.
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)
source](https://postgrest.com/en/stable/install.html#build-from-source)
wiki page.
## Maintenance
### Running Tests
### Schedule
For instructions on running tests, see the official docs hosted here:
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.
https://postgrest.com/en/stable/install.html#postgrest-test-suite
+11
View File
@@ -0,0 +1,11 @@
### Environment
* PostgreSQL version: (if using docker, specify the image)
* PostgREST version: (if using docker, specify the image)
* Operating system:
### Description of issue
(Expected behavior vs actual behavior)
(Steps to reproduce: Include a minimal SQL definition plus how you make the request to PostgREST and the response body)
+6 -1
View File
@@ -1,3 +1,4 @@
.DS_Store
db
dist
.cabal-sandbox
@@ -5,5 +6,9 @@ cabal.sandbox.config
hscope.out
codex.tags
.anvil
.stack-work
.stack-work*
tags
site
*~
*#*
.#*
+190
View File
@@ -0,0 +1,190 @@
# stylish-haskell configuration file
# ==================================
# The stylish-haskell tool is mainly configured by specifying steps. These steps
# are a list, so they have an order, and one specific step may appear more than
# once (if needed). Each file is processed by these steps in the given order.
steps:
# Convert some ASCII sequences to their Unicode equivalents. This is disabled
# by default.
# - unicode_syntax:
# # In order to make this work, we also need to insert the UnicodeSyntax
# # language pragma. If this flag is set to true, we insert it when it's
# # not already present. You may want to disable it if you configure
# # language extensions using some other method than pragmas. Default:
# # true.
# add_language_pragma: true
# Align the right hand side of some elements. This is quite conservative
# and only applies to statements where each element occupies a single
# line.
- simple_align:
cases: true
top_level_patterns: true
records: true
# Import cleanup
- imports:
# There are different ways we can align names and lists.
#
# - global: Align the import names and import list throughout the entire
# file.
#
# - file: Like global, but don't add padding when there are no qualified
# imports in the file.
#
# - group: Only align the imports per group (a group is formed by adjacent
# import lines).
#
# - none: Do not perform any alignment.
#
# Default: global.
align: global
# Folowing options affect only import list alignment.
#
# List align has following options:
#
# - after_alias: Import list is aligned with end of import including
# 'as' and 'hiding' keywords.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - with_alias: Import list is aligned with start of alias or hiding.
#
# > import qualified Data.List as List (concat, foldl, foldr, head,
# > init, last, length)
#
# - new_line: Import list starts always on new line.
#
# > import qualified Data.List as List
# > (concat, foldl, foldr, head, init, last, length)
#
# Default: after_alias
list_align: after_alias
# Long list align style takes effect when import is too long. This is
# determined by 'columns' setting.
#
# - inline: This option will put as much specs on same line as possible.
#
# - new_line: Import list will start on new line.
#
# - new_line_multiline: Import list will start on new line when it's
# short enough to fit to single line. Otherwise it'll be multiline.
#
# - multiline: One line per import list entry.
# Type with contructor list acts like single import.
#
# > import qualified Data.Map as M
# > ( empty
# > , singleton
# > , ...
# > , delete
# > )
#
# Default: inline
long_list_align: inline
# Align empty list (importing instances)
#
# Empty list align has following options
#
# - inherit: inherit list_align setting
#
# - right_after: () is right after the module name:
#
# > import Vector.Instances ()
#
# Default: inherit
empty_list_align: inherit
# List padding determines indentation of import list on lines after import.
# This option affects 'long_list_align'.
#
# - <integer>: constant value
#
# - module_name: align under start of module name.
# Useful for 'file' and 'group' align settings.
list_padding: 4
# Separate lists option affects formating of import list for type
# or class. The only difference is single space between type and list
# of constructors, selectors and class functions.
#
# - true: There is single space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable (fold, foldl, foldMap))
#
# - false: There is no space between Foldable type and list of it's
# functions.
#
# > import Data.Foldable (Foldable(fold, foldl, foldMap))
#
# Default: true
separate_lists: true
# Language pragmas
- language_pragmas:
# We can generate different styles of language pragma lists.
#
# - vertical: Vertical-spaced language pragmas, one per line.
#
# - compact: A more compact style.
#
# - compact_line: Similar to compact, but wrap each line with
# `{-#LANGUAGE #-}'.
#
# Default: vertical.
style: vertical
# Align affects alignment of closing pragma brackets.
#
# - true: Brackets are aligned in same collumn.
#
# - false: Brackets are not aligned together. There is only one space
# between actual import and closing bracket.
#
# Default: true
align: true
# stylish-haskell can detect redundancy of some language pragmas. If this
# is set to true, it will remove those redundant pragmas. Default: true.
remove_redundant: true
# Replace tabs by spaces. This is disabled by default.
# - tabs:
# # Number of spaces to use for each tab. Default: 8, as specified by the
# # Haskell report.
# spaces: 8
# Remove trailing whitespace
- trailing_whitespace: {}
# A common setting is the number of columns (parts of) code will be wrapped
# to. Different steps take this into account. Default: 80.
columns: 70
# By default, line endings are converted according to the OS. You can override
# preferred format here.
#
# - native: Native newline format. CRLF on Windows, LF on other OSes.
#
# - lf: Convert to LF ("\n").
#
# - crlf: Convert to CRLF ("\r\n").
#
# Default: native.
newline: native
# Sometimes, language extensions are specified in a cabal file or from the
# command line instead of using language pragmas in the file. stylish-haskell
# needs to be aware of these, so it can parse the file correctly.
#
# No language extensions are enabled by default.
language_extensions:
- TemplateHaskell
- QuasiQuotes
- CPP
+64
View File
@@ -0,0 +1,64 @@
language: generic
sudo: false
os:
- osx
cache:
timeout: 1000
directories:
- $HOME/.stack
- $HOME/.local/bin
before_install:
- mkdir -p "$HOME/.local/bin"
- export PATH="$PATH:$HOME/.local/bin"
install:
- |
if test -f "$HOME/.local/bin/stack"
then
echo 'Stack is already installed.'
else
echo "Installing Stack..."
travis_retry curl -L https://www.stackage.org/stack/osx-x86_64 > stack.tar.gz
gunzip stack.tar.gz
tar -x -f stack.tar --strip-components 1
mv stack "$HOME/.local/bin/"
rm stack.tar
fi
- |
if test -f "$HOME/.local/bin/ghr"
then
echo 'ghr is already installed.'
else
echo "Installing ghr..."
travis_retry curl -L https://github.com/tcnksm/ghr/releases/download/v0.5.4/ghr_v0.5.4_darwin_386.zip > ghr.zip
unzip ghr.zip -d "$HOME/.local/bin"
rm ghr.zip
fi
script:
- gtimeout 1800 stack build --no-terminal --only-snapshot --install-ghc || true
- |
if test ! "$TRAVIS_TAG"
then
echo 'No tag pushed. Skipping build.'
else
stack build --no-terminal --copy-bins --local-bin-path .
fi
- |
if test ! "$TRAVIS_TAG"
then
echo 'No tag pushed. Skipping release.'
else
OWNER="$(echo "$TRAVIS_REPO_SLUG" | cut -f1 -d/)"
REPO="$(echo "$TRAVIS_REPO_SLUG" | cut -f2 -d/)"
START=$(echo $TRAVIS_TAG | cut -c2-)
END='## \['
BODY=$(sed -n "1,/$START/d;/$END/q;p" CHANGELOG.md)
strip postgrest
tar cjf postgrest-$TRAVIS_TAG-osx.tar.xz postgrest
ghr -t $GITHUB_TOKEN -u $OWNER -r $REPO -b "$BODY"--replace $TRAVIS_TAG postgrest-$TRAVIS_TAG-osx.tar.xz
fi
+14
View File
@@ -0,0 +1,14 @@
# Sponsors & Backers
PostgREST ongoing development is only possible thanks to our Sponsors and Backers, listed below. If you'd like to join them, you can do so by supporting the PostgREST organization on [Patreon](https://www.patreon.com/postgrest).
## Lead Backers
- [Christiaan Westerbeek](https://devotis.nl)
- [Daniel Babiak](https://github.com/d-babiak)
- [Michel Pelletier](https://github.com/michelp/)
## Backers
- Tsingson Qin
- Jay Hannah
+325
View File
@@ -3,6 +3,331 @@
All notable changes to this project will be documented in this file.
This project adheres to [Semantic Versioning](http://semver.org/).
## Unreleased
### Added
### Fixed
## [5.2.0] - 2018-12-12
### Added
- #1205, Add support for parsing JSON Web Key Sets - @russelldavies
- #1203, Add support for reading db-uri from a separate file - @zhoufeng1989
- #1200, Add db-extra-search-path config for adding schemas to the search_path, solves issues related to extensions created on the public schema - @steve-chavez
- #1219, Add ability to quote column names on filters - @steve-chavez
### Fixed
- #1182, Fix embedding on views with composite pks - @steve-chavez
- #1180, Fix embedding on views with subselects in pg10 - @steve-chavez
- #1197, Allow CORS for PUT - @bkylerussell
- #1181, Correctly qualify function argument of custom type in public schema - @steve-chavez
- #1008, Allow columns that contain spaces in filters - @steve-chavez
## [5.1.0] - 2018-08-31
### Added
- #1099, Add support for getting json/jsonb by array index - @steve-chavez
- #1145, Add materialized view columns to OpenAPI output - @steve-chavez
- #709, Allow embedding on views with subselects/CTE - @steve-chavez
- #1148, OpenAPI: add `required` section for the non-nullable columns - @laughedelic
- #1158, Add summary to OpenAPI doc for RPC functions - @mdr1384
### Fixed
- #1113, Fix UPSERT failing when having a camel case PK column - @steve-chavez
- #945, Fix slow start-up time on big schemas - @steve-chavez
- #1129, Fix view embedding when table is capitalized - @steve-chavez
- #1149, OpenAPI: Change `GET` response type to array - @laughedelic
- #1152, Fix RPC failing when having arguments with reserved or uppercase keywords - @mdr1384
- #905, Fix intermittent empty replies - @steve-chavez
- #1139, Fix JWTIssuedAtFuture failure for valid iat claim - @steve-chavez
- #1141, Fix app.settings resetting on pool timeout - @steve-chavez
### Changed
- #1099, Numbers in json path `?select=data->1->>key` now get treated as json array indexes instead of keys - @steve-chavez
- #1128, Allow finishing a json path with a single arrow `->`. Now a json can be obtained without resorting to casting, Previously: `/json_arr?select=data->>2::json`, now: `/json_arr?select=data->2` - @steve-chavez
- #724, Change server-host default of *4 to 127.0.0.1
### Deprecated
- #724, SIGHUP deprecated, SIGUSR1 should be used instead
## [0.5.0.0] - 2018-05-14
### Added
- The configuration (e.g. `postgrest.conf`) now accepts arbitrary settings that will be passed through as session-local database settings. This can be used to pass in secret keys directly as strings, or via OS environment variables. For instance: `app.settings.jwt_secret = "$(MYAPP_JWT_SECRET)"` will take `MYAPP_JWT_SECRET` from the environment and make it available to postgresql functions as `current_setting('app.settings.jwt_secret')`. Only `app.settings.*` values in the configuration file are treated in this way. - @canadaduane
- #256, Add support for bulk UPSERT with POST and single UPSERT with PUT - @steve-chavez
- #1078, Add ability to specify source column in embed - @steve-chavez
- #821, Allow embeds alias to be used in filters - @steve-chavez
- #906, Add jspath configurable `role-claim-key` - @steve-chavez
- #1061, Add foreign tables to OpenAPI output - @rhyamada
### Fixed
- #828, Fix computed column only working in public schema - @steve-chavez
- #925, Fix RPC high memory usage by using parametrized query and avoiding json encoding - @steve-chavez
- #987, Fix embedding with self-reference foreign key - @steve-chavez
- #1044, Fix view parent embedding when having many views - @steve-chavez
- #781, Fix accepting misspelled desc/asc ordering modificators - @onporat, @steve-chavez
### Changed
- #828, A `SET SCHEMA <db-schema>` is done on each request, this has the following implications:
- Computed columns now only work if they belong to the db-schema
- Stored procedures might require a `search_path` to work properly, for further details see https://postgrest.org/en/v5.0/api.html#explicit-qualification
- To use RPC now the `json_to_record/json_to_recordset` functions are needed, these are available starting from PostgreSQL 9.4 - @steve-chavez
- Overloaded functions now depend on the `dbStructure`, restart/sighup may be needed for their correct functioning - @steve-chavez
- #1098, Removed support for:
+ curly braces `{}` in embeds, i.e. `/clients?select=*,projects{*}` can no longer be used, from now on parens `()` should be used `/clients?select=*,projects(*)` - @steve-chavez
+ "in" operator without parens, i.e. `/clients?id=in.1,2,3` no longer supported, `/clients?id=in.(1,2,3)` should be used - @steve-chavez
+ "@@", "@>" and "<@" operators, from now on their mnemonic equivalents should be used "fts", "cs" and "cd" respectively - @steve-chavez
## [0.4.4.0] - 2018-01-08
### Added
- #887, #601, #1007, Allow specifying dictionary and plain/phrase tsquery in full text search - @steve-chavez
- #328, Allow doing GET on rpc - @steve-chavez
- #917, Add ability to map RAISE errorcode/message to http status - @steve-chavez
- #940, Add ability to map GUC to http response headers - @steve-chavez
- #1022, Include git sha in version report - @begriffs
- Faster queries using json_agg - @ruslantalpa
### Fixed
- #876, Read secret files as binary, discard final LF if any - @eric-brechemier
- #968, Treat blank proxy uri as missing - @begriffs
- #933, OpenAPI externals docs url to current version - @steve-chavez
- #962, OpenAPI don't err on nonexistent schema - @steve-chavez
- #954, make OpenAPI rpc output dependent on user privileges - @steve-chavez
- #955, Support configurable aud claim - @statik
- #996, Fix embedded column conflicts table name - @grotsev
- #974, Fix RPC error when function has single OUT param - @steve-chavez
- #1021, Reduce join size in allColumns for faster program start - @nextstopsun
- #411, Remove the need for pk in &select for parent embed - @steve-chavez
- #1016, Fix anonymous requests when configured with jwt-aud - @ruslantalpa
## [0.4.3.0] - 2017-09-06
### Added
- #567, Support more JWT signing algorithms, including RSA - @begriffs
- #889, Allow more than two conditions in a single and/or - @steve-chavez
- #883, Binary output support for RPC - @steve-chavez
- #885, Postgres COMMENTs on SCHEMA/TABLE/COLUMN are used for OpenAPI - @ldesgoui
- #907, Ability to embed using a specific relation when there are multiple between tables - @ruslantalpa
- #930, Split table comment on newline to get OpenAPI operation summary and description - @daurnimator
- #938, Support for range operators - @russelldavies
### Fixed
- #877, Base64 secret read from a file ending with a newline - @eric-brechemier
- #896, Boolean env var interpolation in config file - @begriffs
- #885, OpenAPI repetition reduced by using more definitions- @ldesgoui
- #924, Improve relations initialization time - @9too
- #927, Treat blank pre-request as missing - @begriffs
### Changed
- #938, Deprecate symbol operators with mnemonic names. - @russelldavies
## [0.4.2.0] - 2017-06-11
### Added
- #742, Add connection retrying on startup and SIGHUP - @steve-chavez
- #652, Add and/or params for complex boolean logic - @steve-chavez
- #808, Env var interpolation in config file (helps Docker) - @begriffs
- #878 - CSV output support for RPC - @begriffs
### Fixed
- #822, Treat blank string JWT secret as no secret - @begriffs
## [0.4.1.0] - 2017-04-25
### Added
- Allow requesting binary output on GET - @steve-chavez
- Accept clients requesting `Content-Type: application/json` from / - @feynmanliang
- #493, Updating with empty JSON object makes zero updates @koulakis
- Make HTTP headers and cookies available as GUCs #800 - @ruslantalpa
- #701, Ability to quote values on IN filters - @steve-chavez
- #641, Allow IN filter to have no values - @steve-chavez
### Fixed
- #827, Avoid Warp reaper, extend socket timeout to 1 hour - @majorcode
- #791, malformed nested JSON error - @diogob
- Resource embedding in views referencing tables in public schema - @fab1an
- #777, Empty body is allowed when calling a non-parameterized RPC - @koulakis
- #831, Fix proc resource embedding issue with search_path - @steve-chavez
- #547, Use read-only transaction for stable/immutable RPC - @begriffs
## [0.4.0.0] - 2017-01-19
### Added
- Allow test database to be on another host - @dsimunic
- `Prefer: params=single-object` to treat payload as single json argument in RPC - @dsimunic
- Ability to generate an OpenAPI spec - @mainx07, @hudayou, @ruslantalpa, @begriffs
- Ability to generate an OpenAPI spec behind a proxy - @hudayou
- Ability to set addresses to listen on - @hudayou
- Filtering, shaping and embedding with &select for the /rpc path - @ruslantalpa
- Output names of used-defined types (instead of 'USER-DEFINED') - @martingms
- Implement support for singular representation responses for POST/PATCH requests - @ehamberg
- Include RPC endpoints in OpenAPI output - @begriffs, @LogvinovLeon
- Custom request validation with `--pre-request` argument - @begriffs
- Ability to order by jsonb keys - @steve-chavez
- Ability to specify offset for a deeper level - @ruslantalpa
- Ability to use binary base64 encoded secrets - @TrevorBasinger
### Fixed
- Do not apply limit to parent items - @ruslantalpa
- Fix bug in relation detection when selecting parents two levels up by using the name of the FK - @ruslantalpa
- Customize content negotiation per route - @begriffs
- Allow using nulls order without explicit order direction - @steve-chavez
- Fatal error on postgres unsupported version, format supported version in error message - @steve-chavez
- Prevent database memory cosumption by prepared statements caches - @ruslantalpa
- Use specific columns in the RETURNING section - @ruslantalpa
- Fix columns alias for RETURNING - @steve-chavez
### Changed
- Replace `Prefer: plurality=singular` with `Accept: application/vnd.pgrst.object` - @begriffs
- Standardize arrays in responses for `Prefer: return=representation` - @begriffs
- Calling unknown RPC gives 404, not 400 - @begriffs
- Use HTTP 400 for raise\_exception - @begriffs
- Remove non-OpenAPI schema description - @begriffs
- Use comma rather than semicolon to separate Prefer header values - @begriffs
- Omit total query count by default - @begriffs
- No more reserved `jwt_claims` return type - @begriffs
- HTTP 401 rather than 400 for expired JWT - @begriffs
- Remove default JWT secret - @begriffs
- Use GUC request.jwt.claim.foo rather than postgrest.claims.foo - @begriffs
- Use config file rather than command line arguments - @begriffs
## [0.3.2.0] - 2016-06-10
### Added
- Reload database schema on SIGHUP - @begriffs
- Support "-" in column names - @ruslantalpa
- Support column/node renaming `alias:column` - @ruslantalpa
- Accept posts from HTML forms - @begriffs
- Ability to order embedded entities - @ruslantalpa
- Ability to paginate using &limit and &offset parameters - @ruslantalpa
- Ability to apply limits to embedded entities and enforce --max-rows on all levels - @ruslantalpa, @begriffs
- Add allow response header in OPTIONS - @begriffs
### Fixed
- Return 401 or 403 for access denied rather than 404 - @begriffs
- Omit Content-Type header for empty body - @begriffs
- Prevent role from being changed twice - @begriffs
- Use read-only transaction for read requests - @ruslantalpa
- Include entities from the same parent table using two different foreign keys - @ruslantalpa
- Ensure that Location header in 201 response is URL-encoded - @league
- Fix garbage collector CPU leak - @ruslantalpa et al.
- Return deleted items when return=representation header is sent - @ruslantalpa
- Use table default values for empty object inserts - @begriffs
## [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
+67 -113
View File
@@ -1,51 +1,39 @@
![Logo](static/logo.png "Logo")
[![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">
[![Donate](https://img.shields.io/badge/Donate-Patreon-orange.svg?colorB=F96854)](https://www.patreon.com/postgrest)
[![Donate](https://img.shields.io/badge/Donate-PayPal-green.svg)](https://www.paypal.me/postgrest)
<a href="https://heroku.com/deploy?template=https://github.com/PostgREST/postgrest">
<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)
[![Docs](https://img.shields.io/badge/docs-latest-brightgreen.svg?style=flat)](http://postgrest.org)
[![Docker Stars](https://img.shields.io/docker/pulls/postgrest/postgrest.svg)](https://hub.docker.com/r/postgrest/postgrest/)
[![Build Status](https://circleci.com/gh/PostgREST/postgrest/tree/master.svg?style=shield)](https://circleci.com/gh/PostgREST/postgrest/tree/master)
[![Hackage docs](https://img.shields.io/hackage/v/postgrest.svg?label=hackage)](http://hackage.haskell.org/package/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](http://begriffs.com/posts/2014-12-30-intro-to-postgrest.html) | [GUI Demo](http://marmelab.com/ng-admin-postgrest)
## Usage
Try making requests to the live demo server with an HTTP client
such as [postman](http://www.getpostman.com/). The structure of the
demo database is defined by
[begriffs/postgrest-example](https://github.com/begriffs/postgrest-example).
You can use it as inspiration for test-driven server migrations in
your own projects.
1. Download the binary ([latest release](https://github.com/PostgREST/postgrest/releases/latest))
for your platform.
2. Invoke for help:
### Usage
```bash
postgrest --help
```
Download the binary ([latest release](https://github.com/begriffs/postgrest/releases/latest)) and invoke like so:
## Performance
```bash
postgrest --db-host localhost --db-port 5432 \
--db-name my_db --db-user postgres \
--db-pass foobar --db-pool 200 \
--anonymous postgres --port 3000 \
--v1schema public
```
In production include the `--secure` option which redirects all
requests to HTTPS. Note that PostgREST does not handle the SSL
internally and must be put behind another server that does (such
as nginx or the Heroku load balancer).
### Performance
TLDR; subsecond response times for up to 2000 requests/sec on Heroku free tier. ([see the load test](https://github.com/begriffs/postgrest/wiki/Performance-and-Scaling))
If you're used to servers written in interpreted languages (or named
after precious gems), prepare to be pleasantly surprised by PostgREST
performance.
TLDR; subsecond response times for up to 2000 requests/sec on Heroku
free tier. 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
@@ -61,68 +49,52 @@ Finally it uses the database efficiently with the
[Hasql](https://nikita-volkov.github.io/hasql-benchmarks/) library
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).
## Security
Other optimizations are possible, and some are outlined in the
[Future Features](#future-features).
PostgREST [handles
authentication](http://postgrest.org/en/stable/auth.html) (via 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.
### Security
PostgREST handles authentication (HTTP Basic over SSL or [JSON Web
Tokens](https://github.com/begriffs/postgrest/wiki/Security-and-Permissions#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.
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).
### Versioning
## 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.
To learn more, see the [guide to versioning](https://github.com/begriffs/postgrest/wiki/API-Versioning).
## Self-documentation
### Self-documention
PostgREST uses the [OpenAPI](https://openapis.org/) standard to
generate up-to-date documentation for APIs. You can use a tool like
[Swagger-UI](https://github.com/swagger-api/swagger-ui) to render
interactive documentation for demo requests against the live API server.
Rather than writing and maintaining separate docs yourself let the
API explain its own affordances using HTTP. All PostgREST endpoints
respond to the OPTIONS verb and explain what they support as well
as the data format of their JSON payload.
The number of rows returned by an endpoint is reported by - and
limited with - range headers. More about
This project uses HTTP to communicate other metadata as well. 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
Features](#future-features).
### Data Integrity
## Data Integrity
Rather than relying on an Object Relational Mapper and custom
imperative coding, this system requires you put declarative constraints
@@ -130,46 +102,28 @@ directly into your database. Hence no application can corrupt your
data (including your API server).
The PostgREST exposes HTTP interface with safeguards to prevent
surprises, such as enforcing idempotent PUT requests, and
surprises, such as enforcing idempotent PUT requests.
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 [API guide](http://postgrest.org/en/stable/api.html).
### Future Features
## Supporting development
* Watching endpoint changes with sockets and Postgres pubsub
* Specifying per-view HTTP caching
* Inferring good default caching policies from the Postgres stats collector
* Generating mock data for test clients
* Maintaining separate connection pools per role to avoid "set/reset
role" performance penalty
* Describe more relationships with Link headers
* Depending on accept headers, render OPTIONS as [RAML](http://raml.org/) or a
relational diagram
* Add two-legged auth with OAuth 1.0a(?)
* ... the other [issues](https://github.com/begriffs/postgrest/issues)
You can help PostgREST ongoing maintenance and development by:
### Guides
- Making a regular donation through Patreon https://www.patreon.com/postgrest
* [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)
* [Tutorial](http://blog.jonharrington.org/postgrest-introduction/) (external)
* [Heroku](https://github.com/begriffs/postgrest/wiki/Heroku)
- Alternatively, you can make a one-time donation via Paypal https://www.paypal.me/postgrest
### Thanks
Every donation will be spent on making PostgREST better for the whole community.
* [Adam Baker](https://github.com/adambaker) for code
contributions and many fundamental design discussions
* [Diogo Biazus](https://github.com/diogob) for many improvements
and deep postgresql knowledge
* [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
* [Jonathan Harrington](https://github.com/prio) for writing a [nice
tutorial](http://blog.jonharrington.org/postgrest-introduction/)
* [Federico Rampazzo](https://github.com/framp) for suggesting and
implementing [JWT](http://jwt.io/) support
## Thanks
The PostgREST organization is grateful to:
- The project [sponsors and backers](https://github.com/PostgREST/postgrest/blob/master/BACKERS.md) who support PostgREST's development.
- The project [contributors](https://github.com/PostgREST/postgrest/graphs/contributors) who have improved PostgREST immensely with their code
and good judgement. See more details in the [changelog](https://github.com/PostgREST/postgrest/blob/master/CHANGELOG.md).
The cool logo came from [Mikey Casalaina](https://github.com/casalaina).
+36 -33
View File
@@ -1,56 +1,59 @@
{
"name": "PostgREST",
"description": "RESTful API for any PostgreSQL database.",
"logo": "https://halcyon.sh/logo.svg",
"repository": "https://github.com/begriffs/postgrest",
"logo": "https://avatars2.githubusercontent.com/u/15115011",
"repository": "https://github.com/PostgREST/postgrest",
"env": {
"BUILDPACK_URL": {
"description": "Heroku buildpack for deploying Haskell applications",
"value": "https://github.com/begriffs/postgrest-heroku"
"value": "https://github.com/PostgREST/postgrest-heroku"
},
"POSTGREST_VER": {
"description": "Version of PostgREST to deploy",
"value": "0.2.11.1"
"value": "5.2.0"
},
"DB_NAME": {
"description": "Database name",
"DB_URI": {
"description": "Database connection string, e.g. postgres://user:pass@xxxxxxx.rds.amazonaws.com/mydb",
"required": true
},
"AUTH_ROLE": {
"description": "Database role to use checking client authentication",
"DB_SCHEMA": {
"description": "The database schema to expose to REST clients. Tables, views and stored procedures in this schema will get API endpoints",
"required": true,
"value": "public"
},
"DB_ANON_ROLE": {
"description": "The database role to use when executing commands on behalf of unauthenticated clients",
"required": true
},
"AUTH_PASS": {
"description": "Authentication password",
"required": false
},
"ANONYMOUS_ROLE": {
"description": "Database role for non-authenticated requests",
"required": true
},
"DB_HOST": {
"description": "Database server hostname",
"required": true
},
"DB_PORT": {
"description": "Database server port",
"required": false,
"value": "5432"
},
"DB_POOL": {
"description": "Maximum number of connections in database pool",
"description": "Number of connections to keep open in PostgRESTs database pool",
"required": false,
"value": "10"
},
"JWT_SECRET": {
"description": "Secret used to encrypt JSON Web Tokens",
"required": false,
"value": "secret"
"SERVER_PROXY_URI": {
"description": "Overrides the base URL used within the OpenAPI self-documentation hosted at the API root path",
"required": false
},
"V1SCHEMA": {
"description": "DB schema selected whe no version (or version 1) requested",
"JWT_SECRET": {
"description": "The secret used to decode JWT tokens clients provide for authentication",
"required": false
},
"SECRET_IS_BASE64": {
"description": "When this is set to true, the value derived from jwt-secret will be treated as a base64 encoded secret",
"required": false,
"value": "1"
"value": "false"
},
"JWT_AUD": {
"description": "The audience that should be validated if the JWT token contains an aud claim",
"required": false
},
"MAX_ROWS": {
"description": "A hard limit to the number of rows PostgREST will fetch from a view, table, or stored procedure",
"required": false
},
"PRE_REQUEST": {
"description": "A schema-qualified stored procedure name to call right after switching roles for a client request",
"required": false
}
}
}
+38
View File
@@ -0,0 +1,38 @@
platform: x64
cache:
- "c:\\sr"
- .stack-work
environment:
global:
STACK_ROOT: "c:\\sr"
GOPATH: c:\gopath
test: off
skip_non_tags: true
skip_branch_with_pr: true
branches:
only:
- master
install:
- set PATH=C:\Program Files\PostgreSQL\9.6\bin\;%PATH%
- curl -sS -ostack.zip -L --insecure http://www.stackage.org/stack/windows-x86_64
- 7z x stack.zip stack.exe
- set PATH=%GOPATH%\bin;c:\go\bin;%PATH%
- go get -u github.com/tcnksm/ghr
build_script:
- stack setup --no-terminal > nul
- stack build -j1 --copy-bins --local-bin-path .
artifacts:
- path: postgrest.exe
deploy_script:
- 7z a -tzip postgrest-%APPVEYOR_REPO_TAG_NAME%-windows-x64.zip postgrest.exe
- bash -lc "exec 0</dev/null && cd $APPVEYOR_BUILD_FOLDER && ghr -t $GITHUB_TOKEN -u $APPVEYOR_ACCOUNT_NAME -r $APPVEYOR_PROJECT_NAME -b \"$(sed -n \"1,/$(echo $APPVEYOR_REPO_TAG_NAME | cut -c2-)/d;/## \[/q;p\" CHANGELOG.md)\" --replace $APPVEYOR_REPO_TAG_NAME postgrest-$APPVEYOR_REPO_TAG_NAME-windows-x64.zip"
-16
View File
@@ -1,16 +0,0 @@
machine:
pre:
- createuser --superuser --no-password postgrest_test
- createdb -O postgrest_test -U ubuntu postgrest_test
ghc:
version: 7.8.3
dependencies:
override:
- cabal update
- cabal sandbox init
- cabal install --upgrade-dependencies --constraint="template-haskell installed" --dependencies-only --enable-tests
- cabal configure --enable-tests -f ci
test:
post:
- cabal exec hlint -- -X QuasiQuotes src/**/*.hs test/**/*.hs
- cabal exec packdeps postgrest.cabal
-8
View File
@@ -1,8 +0,0 @@
#!/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 &
-23
View File
@@ -1,23 +0,0 @@
# run service as
#POSTGREST_USER=postgrest
# log file
#POSTGREST_LOG=/var/log/postgrest/postgrest.log
# database host
#POSTGREST_DBHOST=localhost
# database to use
#POSTGREST_DBNAME=
# database user
#POSTGREST_DBUSER=postgres
# database password
#POSTGREST_DBPASS=
# database pool
#POSTGREST_DBPOOL=10
# additional options
#POSTGREST_OPTS=
-78
View File
@@ -1,78 +0,0 @@
#!/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
POSTGREST_USER=${POSTGREST_USER:-postgrest}
POSTGREST_DBNAME=${POSTGREST_DBNAME:-postgres}
POSTGREST_DBUSER=${POSTGREST_DBUSER:-postgres}
if [ -n "$POSTGREST_DBHOST" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-host $POSTGREST_DBHOST"
fi
if [ -n "$POSTGREST_DBNAME" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-name $POSTGREST_DBNAME"
fi
if [ -n "$POSTGREST_DBUSER" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-user $POSTGREST_DBUSER"
POSTGREST_OPTS="$POSTGREST_OPTS --anonymous $POSTGREST_DBUSER"
fi
if [ -n "$POSTGREST_DBPASS" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-pass $POSTGREST_DBPASS"
fi
if [ -n "$POSTGREST_DBPOOL" ]; then
POSTGREST_OPTS="$POSTGREST_OPTS --db-pool $POSTGREST_DBPOOL"
fi
POSTGREST_OPTS="$POSTGREST_OPTS --v1schema public"
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 -- $POSTGREST_OPTS; 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
+45
View File
@@ -0,0 +1,45 @@
FROM debian:jessie
ARG POSTGREST_VERSION
# Install libpq5
RUN apt-get -qq update && \
apt-get -qq install -y --no-install-recommends libpq5 && \
apt-get -qq clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# Install postgrest
RUN BUILD_DEPS="curl ca-certificates xz-utils" && \
apt-get -qq update && \
apt-get -qq install -y --no-install-recommends $BUILD_DEPS && \
cd /tmp && \
curl -SLO https://github.com/PostgREST/postgrest/releases/download/${POSTGREST_VERSION}/postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
tar -xJvf postgrest-${POSTGREST_VERSION}-ubuntu.tar.xz && \
mv postgrest /usr/local/bin/postgrest && \
cd / && \
apt-get -qq purge --auto-remove -y $BUILD_DEPS && \
apt-get -qq clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
COPY postgrest.conf /etc/postgrest.conf
ENV PGRST_DB_URI= \
PGRST_DB_SCHEMA=public \
PGRST_DB_ANON_ROLE= \
PGRST_DB_POOL=100 \
PGRST_SERVER_HOST=*4 \
PGRST_SERVER_PORT=3000 \
PGRST_SERVER_PROXY_URI= \
PGRST_JWT_SECRET= \
PGRST_SECRET_IS_BASE64=false \
PGRST_JWT_AUD= \
PGRST_MAX_ROWS= \
PGRST_PRE_REQUEST= \
PGRST_ROLE_CLAIM_KEY=".role"
# PostgREST reads /etc/postgrest.conf so map the configuration
# file in when you run this container
CMD exec postgrest /etc/postgrest.conf
EXPOSE 3000
+3
View File
@@ -0,0 +1,3 @@
db-uri = "postgres://app_user:password@postgres:5432/app_db"
db-schema = "public"
db-anon-role = "app_user"
+18
View File
@@ -0,0 +1,18 @@
FROM centos:centos6
RUN yum -y update
RUN yum -y install perl make automake gcc gmp-devel libffi zlib zlib-devel xz tar
RUN yum -y install https://download.postgresql.org/pub/repos/yum/9.3/redhat/rhel-6-x86_64/pgdg-centos93-9.3-2.noarch.rpm
RUN yum -y install postgresql93-devel
RUN yum clean all
RUN curl -sSL https://get.haskellstack.org/ | sh
ENV PATH $PATH:/usr/pgsql-9.3/bin
# To disable warning when building
ENV PATH $PATH:/root/.local/bin
RUN mkdir /source
WORKDIR /source
ENTRYPOINT ["stack"]
+18
View File
@@ -0,0 +1,18 @@
FROM centos:centos7
RUN yum -y update
RUN yum -y install perl make automake gcc gmp-devel libffi zlib zlib-devel xz tar
RUN yum -y install yum install https://download.postgresql.org/pub/repos/yum/9.3/redhat/rhel-7-x86_64/pgdg-centos93-9.3-2.noarch.rpm
RUN yum -y install postgresql93-devel
RUN yum clean all
RUN curl -sSL https://get.haskellstack.org/ | sh
ENV PATH $PATH:/usr/pgsql-9.3/bin
# To disable warning when building
ENV PATH $PATH:/root/.local/bin
RUN mkdir /source
WORKDIR /source
ENTRYPOINT ["stack"]
+18
View File
@@ -0,0 +1,18 @@
FROM ubuntu:16.04
RUN BUILD_DEPS="curl ca-certificates build-essential" && \
apt-get -qq update && \
apt-get -qqy --no-install-recommends install \
$BUILD_DEPS \
libpq-dev && \
curl -sSL https://get.haskellstack.org/ | sh && \
apt-get -qq clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# To disable warning when building
ENV PATH $PATH:/root/.local/bin
RUN mkdir /source
WORKDIR /source
ENTRYPOINT ["stack"]
@@ -0,0 +1,18 @@
FROM 32bit/ubuntu:16.04
RUN BUILD_DEPS="curl ca-certificates build-essential" && \
apt-get -qq update && \
apt-get -qqy --no-install-recommends install \
$BUILD_DEPS \
libpq-dev && \
curl -sSL https://get.haskellstack.org/ | sh && \
apt-get -qq clean && \
rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
# To disable warning when building
ENV PATH $PATH:/root/.local/bin
RUN mkdir /source
WORKDIR /source
ENTRYPOINT ["stack"]
+19
View File
@@ -0,0 +1,19 @@
stgrest:
image: pg_local
ports:
- "3000:3000"
links:
- postgres:postgres
environment:
PGRST_DB_URI: postgres://app_user:password@postgres:5432/app_db
PGRST_DB_SCHEMA: public
PGRST_DB_ANON_ROLE: app_user
postgres:
image: postgres
ports:
- "5432:5432"
environment:
POSTGRES_DB: app_db
POSTGRES_USER: app_user
POSTGRES_PASSWORD: password
+16
View File
@@ -0,0 +1,16 @@
db-uri = "$(PGRST_DB_URI)"
db-schema = "$(PGRST_DB_SCHEMA)"
db-anon-role = "$(PGRST_DB_ANON_ROLE)"
db-pool = "$(PGRST_DB_POOL)"
server-host = "$(PGRST_SERVER_HOST)"
server-port = "$(PGRST_SERVER_PORT)"
server-proxy-uri = "$(PGRST_SERVER_PROXY_URI)"
jwt-secret = "$(PGRST_JWT_SECRET)"
secret-is-base64 = "$(PGRST_SECRET_IS_BASE64)"
jwt-aud = "$(PGRST_JWT_AUD)"
role-claim-key = "$(PGRST_ROLE_CLAIM_KEY)"
max-rows = "$(PGRST_MAX_ROWS)"
pre-request = "$(PGRST_PRE_REQUEST)"
+295
View File
@@ -0,0 +1,295 @@
{-# LANGUAGE CPP #-}
module Main where
import PostgREST.App (postgrest)
import PostgREST.Config (AppConfig (..),
prettyVersion, readOptions)
import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Error (encodeError)
import PostgREST.OpenAPI (isMalformedProxyUri)
import PostgREST.Types (DbStructure, Schema, PgVersion(..), minimumPgVersion)
import Protolude hiding (hPutStrLn, replace)
import Control.AutoUpdate (defaultUpdateSettings,
mkAutoUpdate, updateAction)
import Control.Retry (RetryStatus, capDelay,
exponentialBackoff,
retrying, rsPreviousDelay)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Base64 as B64
import Data.IORef (IORef, atomicWriteIORef,
newIORef, readIORef)
import Data.String (IsString (..))
import Data.Text (pack, replace, stripPrefix, strip)
import Data.Text.Encoding (decodeUtf8, encodeUtf8)
import Data.Text.IO (hPutStrLn, readFile)
import Data.Time.Clock (getCurrentTime)
import qualified Hasql.Pool as P
import qualified Hasql.Session as H
import qualified Hasql.Transaction.Sessions as HT
import Network.Wai.Handler.Warp (defaultSettings,
runSettings, setHost,
setPort, setServerName)
import System.IO (BufferMode (..),
hSetBuffering)
#ifndef mingw32_HOST_OS
import System.Posix.Signals
#endif
{-|
The purpose of this worker is to fill the refDbStructure created in 'main'
with the 'DbStructure' returned from calling 'getDbStructure'. This method
is meant to be called by multiple times by the same thread, but does nothing if
the previous invocation has not terminated. In all cases this method does not
halt the calling thread, the work is preformed in a separate thread.
Note: 'atomicWriteIORef' is essentially a lazy semaphore that prevents two
threads from running 'connectionWorker' at the same time.
Background thread that does the following :
1. Tries to connect to pg server and will keep trying until success.
2. Checks if the pg version is supported and if it's not it kills the main
program.
3. Obtains the dbStructure.
4. If 2 or 3 fail to give their result it means the connection is down so it
goes back to 1, otherwise it finishes his work successfully.
-}
connectionWorker
:: ThreadId -- ^ This thread is killed if pg version is unsupported
-> P.Pool -- ^ The PostgreSQL connection pool
-> Schema -- ^ Schema PostgREST is serving up
-> IORef (Maybe DbStructure) -- ^ mutable reference to 'DbStructure'
-> IORef Bool -- ^ Used as a binary Semaphore
-> IO ()
connectionWorker mainTid pool schema refDbStructure refIsWorkerOn = do
isWorkerOn <- readIORef refIsWorkerOn
unless isWorkerOn $ do
atomicWriteIORef refIsWorkerOn True
void $ forkIO work
where
work = do
atomicWriteIORef refDbStructure Nothing
putStrLn ("Attempting to connect to the database..." :: Text)
connected <- connectingSucceeded pool
when connected $ do
result <- P.use pool $ do
actualPgVersion <- getPgVersion
unless (actualPgVersion >= minimumPgVersion) $ liftIO $ do
hPutStrLn stderr
("Cannot run in this PostgreSQL version, PostgREST needs at least "
<> pgvName minimumPgVersion)
killThread mainTid
dbStructure <- HT.transaction HT.ReadCommitted HT.Read $ getDbStructure schema actualPgVersion
liftIO $ atomicWriteIORef refDbStructure $ Just dbStructure
case result of
Left e -> do
putStrLn ("Failed to query the database. Retrying." :: Text)
hPutStrLn stderr (toS $ encodeError e)
work
Right _ -> do
atomicWriteIORef refIsWorkerOn False
putStrLn ("Connection successful" :: Text)
{-|
Used by 'connectionWorker' to check if the provided db-uri lets
the application access the PostgreSQL database. This method is used
the first time the connection is tested, but only to test before
calling 'getDbStructure' inside the 'connectionWorker' method.
The connection tries are capped, but if the connection times out no error is
thrown, just 'False' is returned.
-}
connectingSucceeded :: P.Pool -> IO Bool
connectingSucceeded pool =
retrying (capDelay 32000000 $ exponentialBackoff 1000000)
shouldRetry
(const $ P.release pool >> isConnectionSuccessful)
where
isConnectionSuccessful :: IO Bool
isConnectionSuccessful = do
testConn <- P.use pool $ H.sql "SELECT 1"
case testConn of
Left e -> hPutStrLn stderr (toS $ encodeError e) >> pure False
_ -> pure True
shouldRetry :: RetryStatus -> Bool -> IO Bool
shouldRetry rs isConnSucc = do
delay <- pure $ fromMaybe 0 (rsPreviousDelay rs) `div` 1000000
itShould <- pure $ not isConnSucc
when itShould $
putStrLn $ "Attempting to reconnect to the database in " <> (show delay::Text) <> " seconds..."
return itShould
{-|
This is where everything starts.
-}
main :: IO ()
main = do
--
-- LineBuffering: the entire output buffer is flushed whenever a newline is
-- output, the buffer overflows, a hFlush is issued or the handle is closed
--
-- NoBuffering: output is written immediately and never stored in the buffer
hSetBuffering stdout LineBuffering
hSetBuffering stdin LineBuffering
hSetBuffering stderr NoBuffering
--
-- readOptions builds the 'AppConfig' from the config file specified on the
-- command line
conf <- loadDbUriFile =<< loadSecretFile =<< readOptions
let host = configHost conf
port = configPort conf
proxy = configProxyUri conf
pgSettings = toS (configDatabase conf) -- is the db-uri
roleClaimKey = configRoleClaimKey conf
appSettings =
setHost ((fromString . toS) host) -- Warp settings
. setPort port
. setServerName (toS $ "postgrest/" <> prettyVersion) $
defaultSettings
-- Checks that the provided proxy uri is formated correctly
when (isMalformedProxyUri $ toS <$> proxy) $
panic
"Malformed proxy uri, a correct example: https://example.com:8443/basePath"
-- Checks that the provided jspath is valid
when (isLeft roleClaimKey) $
panic $ show roleClaimKey
putStrLn $ ("Listening on port " :: Text) <> show (configPort conf)
--
-- create connection pool with the provided settings, returns either
-- a 'Connection' or a 'ConnectionError'. Does not throw.
pool <- P.acquire (configPool conf, 10, pgSettings)
--
-- To be filled in by connectionWorker
refDbStructure <- newIORef Nothing
--
-- Helper ref to make sure just one connectionWorker can run at a time
refIsWorkerOn <- newIORef False
--
-- This is passed to the connectionWorker method so it can kill the main
-- thread if the PostgreSQL's version is not supported.
mainTid <- myThreadId
--
-- Sets the refDbStructure
connectionWorker
mainTid
pool
(configSchema conf)
refDbStructure
refIsWorkerOn
--
-- Only for systems with signals:
--
-- releases the connection pool whenever the program is terminated,
-- see issue #268
--
-- Plus the SIGHUP signal updates the internal 'DbStructure' by running
-- 'connectionWorker' exactly as before.
#ifndef mingw32_HOST_OS
forM_ [sigINT, sigTERM] $ \sig ->
void $ installHandler sig (Catch $ do
P.release pool
throwTo mainTid UserInterrupt
) Nothing
forM_ [sigHUP, sigUSR1] $ \sig ->
void $ installHandler sig (
Catch $ connectionWorker
mainTid
pool
(configSchema conf)
refDbStructure
refIsWorkerOn
) Nothing
#endif
-- ask for the OS time at most once per second
getTime <- mkAutoUpdate defaultUpdateSettings {updateAction = getCurrentTime}
-- run the postgrest application
runSettings appSettings $
postgrest
conf
refDbStructure
pool
getTime
(connectionWorker
mainTid
pool
(configSchema conf)
refDbStructure
refIsWorkerOn)
{-|
The purpose of this function is to load the JWT secret from a file if
configJwtSecret is actually a filepath and replaces some characters if the JWT
is base64 encoded.
The reason some characters need to be replaced is because JWT is actually
base64url encoded which must be turned into just base64 before decoding.
To check if the JWT secret is provided is in fact a file path, it must be
decoded as 'Text' to be processed.
decodeUtf8: Decode a ByteString containing UTF-8 encoded text that is known to
be valid.
-}
loadSecretFile :: AppConfig -> IO AppConfig
loadSecretFile conf = extractAndTransform mSecret
where
mSecret = decodeUtf8 <$> configJwtSecret conf
isB64 = configJwtSecretIsBase64 conf
--
-- The Text (variable name secret) here is mSecret from above which is the JWT
-- decoded as Utf8
--
-- stripPrefix: Return the suffix of the second string if its prefix matches
-- the entire first string.
--
-- The configJwtSecret is a filepath instead of the JWT secret itself if the
-- secret has @ as its prefix.
extractAndTransform :: Maybe Text -> IO AppConfig
extractAndTransform Nothing = return conf
extractAndTransform (Just secret) =
fmap setSecret $
transformString isB64 =<<
case stripPrefix "@" secret of
Nothing -> return . encodeUtf8 $ secret
Just filename -> chomp <$> BS.readFile (toS filename)
where
chomp bs = fromMaybe bs (BS.stripSuffix "\n" bs)
--
-- Turns the Base64url encoded JWT into Base64
transformString :: Bool -> ByteString -> IO ByteString
transformString False t = return t
transformString True t =
case B64.decode $ encodeUtf8 $ strip $ replaceUrlChars $ decodeUtf8 t of
Left errMsg -> panic $ pack errMsg
Right bs -> return bs
setSecret bs = conf {configJwtSecret = Just bs}
--
-- replace: Replace every occurrence of one substring with another
replaceUrlChars =
replace "_" "/" . replace "-" "+" . replace "." "="
{-
Load database uri from a separate file if `db-uri` is a filepath.
-}
loadDbUriFile :: AppConfig -> IO AppConfig
loadDbUriFile conf = extractDbUri mDbUri
where
mDbUri = configDatabase conf
extractDbUri :: Text -> IO AppConfig
extractDbUri dbUri =
fmap setDbUri $
case stripPrefix "@" dbUri of
Nothing -> return dbUri
Just filename -> strip <$> readFile (toS filename)
setDbUri dbUri = conf {configDatabase = dbUri}
+135 -108
View File
@@ -2,19 +2,21 @@ 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.11.1
version: 5.2.0
synopsis: REST API for any Postgres database
license: MIT
license-file: LICENSE
author: Joe Nelson, Adam Baker
homepage: https://github.com/begriffs/postgrest
maintainer: cred+github@begriffs.com
category: Web
homepage: https://postgrest.org
maintainer: Steve Chávez <stevechavezast@gmail.com>
bug-reports: https://github.com/PostgREST/postgrest/issues
category: Executable, PostgreSQL, Network APIs
extra-source-files: CHANGELOG.md
build-type: Simple
cabal-version: >=1.10
source-repository head
type: git
location: git://github.com/begriffs/postgrest.git
location: git://github.com/PostgREST/postgrest.git
Flag CI
Description: No warnings allowed in continuous integration
@@ -22,128 +24,153 @@ Flag CI
Default: False
executable postgrest
main-is: PostgREST/Main.hs
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
main-is: Main.hs
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
ghc-options:
-threaded
-rtsopts
"-with-rtsopts=-N -I2"
default-language: Haskell2010
build-depends: base >=4.6 && <5
build-depends: auto-update
, base >= 4.8 && < 4.10
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, postgrest
, hasql >= 0.7.3 && < 0.8
, hasql-backend >= 0.4.1 && < 0.5
, hasql-postgres >= 0.10.4 && < 0.11
, warp >= 3.0.2, wai >= 3.0.1
, wai-extra, wai-cors
, wai-middleware-static >= 0.6.0
, HTTP, convertible, http-types
, case-insensitive
, scientific, time
, aeson >= 0.8, network >= 2.6
, bytestring, text, split, string-conversions
, stringsearch
, containers, unordered-containers
, optparse-applicative == 0.11.*
, regex-base, regex-tdfa
, Ranged-sets
, transformers, MissingH
, bcrypt >= 0.0.6, base64-string
, network-uri >= 2.6
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, jwt
hs-source-dirs: src
, protolude == 0.2.2
, text
, time
, warp
, bytestring
, base64-bytestring
, retry
if !os(windows)
build-depends: unix
hs-source-dirs: main
library
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
default-language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
build-depends: base >=4.6 && <5
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, wai-extra, wai-cors
, wai-middleware-static
, HTTP, convertible, http-types
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
build-depends: aeson
, ansi-wl-pprint
, base >= 4.8 && < 4.10
, base64-bytestring
, bytestring
, case-insensitive
, scientific, time
, aeson, network
, bytestring, text, split, string-conversions
, stringsearch
, containers, unordered-containers
, optparse-applicative
, regex-base, regex-tdfa
, Ranged-sets
, transformers, MissingH
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, jwt
Exposed-Modules: PostgREST.App
, configurator-ng == 0.0.0.1
, containers
, contravariant
, contravariant-extras
, either
, gitrev
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, heredoc
, HTTP
, http-types
, insert-ordered-containers
, interpolatedstring-perl6
, jose == 0.7.0.0
, lens
, lens-aeson
, network-uri
, optparse-applicative >= 0.13 && < 0.15
, parsec
, protolude == 0.2.2
, Ranged-sets == 0.3.0
, regex-tdfa
, scientific
, swagger2
, text
, time
, unordered-containers
, vector
, wai
, wai-cors
, wai-extra
, wai-middleware-static
, cookie
Other-Modules: Paths_postgrest
Exposed-Modules: PostgREST.ApiRequest
, PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.DbStructure
, PostgREST.DbRequestBuilder
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.PgQuery
, PostgREST.PgStructure
, PostgREST.OpenAPI
, PostgREST.Parsers
, PostgREST.QueryBuilder
, PostgREST.RangeQuery
, PostgREST.Types
hs-source-dirs: src
Test-Suite spec
Type: exitcode-stdio-1.0
Default-Language: Haskell2010
default-extensions: OverloadedStrings, ScopedTypeVariables, QuasiQuotes
Hs-Source-Dirs: test, src
if flag(ci)
ghc-options: -Wall -W -Werror
else
ghc-options: -Wall -W -O2
default-extensions: OverloadedStrings, QuasiQuotes, NoImplicitPrelude
ghc-options: -threaded -rtsopts -with-rtsopts=-N
Hs-Source-Dirs: test
Main-Is: Main.hs
Other-Modules: PostgREST.App
, PostgREST.Auth
, PostgREST.Config
, PostgREST.Error
, PostgREST.Middleware
, PostgREST.PgQuery
, PostgREST.PgStructure
, PostgREST.RangeQuery
, Spec
Other-Modules: Feature.AudienceJwtSecretSpec
, Feature.AuthSpec
, Feature.AsymmetricJwtSpec
, Feature.BinaryJwtSecretSpec
, Feature.ConcurrentSpec
, Feature.CorsSpec
, Feature.DeleteSpec
, Feature.ExtraSearchPathSpec
, Feature.InsertSpec
, Feature.JsonOperatorSpec
, Feature.NoJwtSpec
, Feature.PgVersion95Spec
, Feature.PgVersion96Spec
, Feature.ProxySpec
, Feature.QueryLimitedSpec
, Feature.QuerySpec
, Feature.RangeSpec
, Feature.SingularSpec
, Feature.StructureSpec
, Feature.UnicodeSpec
, Feature.AndOrParamsSpec
, Feature.RpcSpec
, Feature.NonexistentSchemaSpec
, Feature.UpsertSpec
, SpecHelper
Build-Depends: base, hspec == 2.1.*, QuickCheck
, hspec-wai, hspec-wai-json
, hasql, hasql-backend
, hasql-postgres
, warp, wai
, packdeps, hlint
, HTTP, convertible
, TestTypes
Build-Depends: aeson
, aeson-qq
, async
, auto-update
, base >= 4.8 && < 4.10
, bytestring
, base64-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
, Ranged-sets
, transformers, MissingH, split
, bcrypt, base64-string
, network-uri
, resource-pool
, blaze-builder
, vector
, mtl
, cassava
, process
, containers
, contravariant
, hasql >= 1.3 && < 1.4
, hasql-pool >= 0.5 && < 0.6
, hasql-transaction >= 0.7 && < 0.8
, heredoc
, jwt
, hjsonschema == 1.5.0.1
, hspec
, hspec-wai >= 0.7.0
, hspec-wai-json
, http-types
, lens
, lens-aeson
, monad-control
, postgrest
, process
, protolude == 0.2.2
, regex-tdfa
, time
, transformers-base
, wai
, wai-extra
+304
View File
@@ -0,0 +1,304 @@
{-# LANGUAGE LambdaCase #-}
{-|
Module : PostgREST.ApiRequest
Description : PostgREST functions to translate HTTP request to a domain type called ApiRequest.
-}
module PostgREST.ApiRequest ( ApiRequest(..)
, ContentType(..)
, Action(..)
, Target(..)
, PreferRepresentation (..)
, mutuallyAgreeable
, userApiRequest
) where
import Protolude
import qualified Data.Aeson as JSON
import Data.Aeson.Types (emptyObject, emptyArray)
import qualified Data.ByteString as BS
import qualified Data.ByteString.Internal as BS (c2w)
import qualified Data.ByteString.Lazy as BL
import qualified Data.Csv as CSV
import qualified Data.List as L
import Data.List (lookup, last, partition)
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import Data.Maybe (fromJust)
import Control.Arrow ((***))
import qualified Data.Text as T
import qualified Data.Vector as V
import Network.HTTP.Base (urlEncodeVars)
import Network.HTTP.Types.Header (hAuthorization, hCookie)
import Network.HTTP.Types.URI (parseSimpleQuery)
import Network.Wai (Request (..))
import Network.Wai.Parse (parseHttpAccept)
import PostgREST.RangeQuery (NonnegRange, rangeRequested, restrictRange, rangeGeq, allRange, rangeLimit, rangeOffset)
import Data.Ranged.Boundaries
import PostgREST.Types
import Data.Ranged.Ranges (Range(..), rangeIntersection, emptyRange)
import qualified Data.CaseInsensitive as CI
import Web.Cookie (parseCookiesText)
type RequestBody = BL.ByteString
-- | Types of things a user wants to do to tables/views/procs
data Action = ActionCreate | ActionRead
| ActionUpdate | ActionDelete
| ActionInfo | ActionInvoke{isReadOnly :: Bool}
| ActionInspect | ActionSingleUpsert
deriving Eq
-- | The target db object of a user action
data Target = TargetIdent QualifiedIdentifier
| TargetProc QualifiedIdentifier
| TargetRoot
| TargetUnknown [Text]
deriving Eq
-- | How to return the inserted data
data PreferRepresentation = Full | HeadersOnly | None deriving Eq
{-|
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 {
-- | Similar but not identical to HTTP verb, e.g. Create/Invoke both POST
iAction :: Action
-- | Requested range of rows within response
, iRange :: M.HashMap ByteString NonnegRange
-- | The target, be it calling a proc or accessing a table
, iTarget :: Target
-- | Content types the client will accept, [CTAny] if no Accept header
, iAccepts :: [ContentType]
-- | Data sent by client and used for mutation actions
, iPayload :: Maybe PayloadJSON
-- | If client wants created items echoed back
, iPreferRepresentation :: PreferRepresentation
-- | Pass all parameters as a single json object to a stored procedure
, iPreferSingleObjectParameter :: Bool
-- | Whether the client wants a result count (slower)
, iPreferCount :: Bool
-- | Whether the client wants to UPSERT or ignore records on PK conflict
, iPreferResolution :: Maybe PreferResolution
-- | Filters on the result ("id", "eq.10")
, iFilters :: [(Text, Text)]
-- | &and and &or parameters used for complex boolean logic
, iLogic :: [(Text, Text)]
-- | &select parameter used to shape the response
, iSelect :: Text
-- | &order parameters for each level
, iOrder :: [(Text, Text)]
-- | Alphabetized (canonical) request query string for response URLs
, iCanonicalQS :: ByteString
-- | JSON Web Token
, iJWT :: Text
-- | HTTP request headers
, iHeaders :: [(Text, Text)]
-- | Request Cookies
, iCookies :: [(Text, Text)]
}
-- | Examines HTTP request and translates it into user intent.
userApiRequest :: Schema -> Request -> RequestBody -> Either ApiRequestError ApiRequest
userApiRequest schema req reqBody
| isTargetingProc && method `notElem` ["GET", "POST"] = Left ActionInappropriate
| topLevelRange == emptyRange = Left InvalidRange
| shouldParsePayload && isLeft payload = either (Left . InvalidBody . toS) witness payload
| otherwise = Right ApiRequest {
iAction = action
, iTarget = target
, iRange = ranges
, iAccepts = maybe [CTAny] (map decodeContentType . parseHttpAccept) $ lookupHeader "accept"
, iPayload = relevantPayload
, iPreferRepresentation = representation
, iPreferSingleObjectParameter = singleObject
, iPreferCount = hasPrefer "count=exact"
, iPreferResolution = if hasPrefer (show MergeDuplicates) then Just MergeDuplicates
else if hasPrefer (show IgnoreDuplicates) then Just IgnoreDuplicates
else Nothing
, iFilters = filters
, iLogic = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["and", "or"] k ]
, iSelect = toS $ fromMaybe "*" $ fromMaybe (Just "*") $ lookup "select" qParams
, iOrder = [(toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, endingIn ["order"] k ]
, iCanonicalQS = toS $ urlEncodeVars
. L.sortBy (comparing fst)
. map (join (***) toS)
. parseSimpleQuery
$ rawQueryString req
, iJWT = tokenStr
, iHeaders = [ (toS $ CI.foldedCase k, toS v) | (k,v) <- hdrs, k /= hAuthorization, k /= hCookie]
, iCookies = maybe [] parseCookiesText $ lookupHeader "Cookie"
}
where
-- rpcQParams = Rpc query params e.g. /rpc/name?param1=val1, similar to filter but with no operator(eq, lt..)
(filters, rpcQParams) =
case action of
ActionInvoke{isReadOnly=True} -> partition (liftM2 (||) (isEmbedPath . fst) (hasOperator . snd)) flts
_ -> (flts, [])
flts = [ (toS k, toS $ fromJust v) | (k,v) <- qParams, isJust v, k /= "select", not (endingIn ["order", "limit", "offset", "and", "or"] k) ]
hasOperator val = any (`T.isPrefixOf` val) $
((<> ".") <$> "not":M.keys operators) ++
((<> "(") <$> M.keys ftsOperators)
isEmbedPath = T.isInfixOf "."
isTargetingProc = (== Just "rpc") $ listToMaybe path
payload =
case (decodeContentType . fromMaybe "application/json" $ lookupHeader "content-type", action) of
(_, ActionInvoke{isReadOnly=True}) ->
Right $ PayloadJSON (JSON.encode $ M.fromList $ second JSON.toJSON <$> rpcQParams) PJObject (S.fromList $ fst <$> rpcQParams)
(CTApplicationJSON, _) ->
note "All object keys must match" . payloadAttributes reqBody
=<< if BL.null reqBody && isTargetingProc
then Right emptyObject
else JSON.eitherDecode reqBody
(CTTextCSV, _) -> do
json <- csvToJson <$> CSV.decodeByName reqBody
note "All lines must have same number of fields" $ payloadAttributes (JSON.encode json) json
(CTOther "application/x-www-form-urlencoded", _) ->
let json = M.fromList . map (toS *** JSON.String . toS) . parseSimpleQuery $ toS reqBody
keys = S.fromList $ M.keys json in
Right $ PayloadJSON (JSON.encode json) PJObject keys
(ct, _) ->
Left $ toS $ "Content-Type not acceptable: " <> toMime ct
topLevelRange = fromMaybe allRange $ M.lookup "limit" ranges
action =
case method of
"GET" | target == TargetRoot -> ActionInspect
| isTargetingProc -> ActionInvoke{isReadOnly=True}
| otherwise -> ActionRead
"POST" -> if isTargetingProc
then ActionInvoke{isReadOnly=False}
else ActionCreate
"PATCH" -> ActionUpdate
"PUT" -> ActionSingleUpsert
"DELETE" -> ActionDelete
"OPTIONS" -> ActionInfo
_ -> ActionInspect
target = case path of
[] -> TargetRoot
[table] -> TargetIdent
$ QualifiedIdentifier schema table
["rpc", proc] -> TargetProc
$ QualifiedIdentifier schema proc
other -> TargetUnknown other
shouldParsePayload = action `elem` [ActionCreate, ActionUpdate, ActionSingleUpsert, ActionInvoke{isReadOnly=False}, ActionInvoke{isReadOnly=True}]
relevantPayload | shouldParsePayload = rightToMaybe payload
| otherwise = Nothing
path = pathInfo req
method = requestMethod req
hdrs = requestHeaders req
qParams = [(toS k, v)|(k,v) <- queryString req]
lookupHeader = flip lookup hdrs
hasPrefer :: Text -> Bool
hasPrefer val = any (\(h,v) -> h == "Prefer" && val `elem` split v) hdrs
where
split :: BS.ByteString -> [Text]
split = map T.strip . T.split (==',') . toS
singleObject = hasPrefer "params=single-object"
representation
| hasPrefer "return=representation" = Full
| hasPrefer "return=minimal" = None
| otherwise = HeadersOnly
auth = fromMaybe "" $ lookupHeader hAuthorization
tokenStr = case T.split (== ' ') (toS auth) of
("Bearer" : t : _) -> t
_ -> ""
endingIn:: [Text] -> Text -> Bool
endingIn xx key = lastWord `elem` xx
where lastWord = last $ T.split (=='.') key
headerRange = rangeRequested hdrs
replaceLast x s = T.intercalate "." $ L.init (T.split (=='.') s) ++ [x]
limitParams :: M.HashMap ByteString NonnegRange
limitParams = M.fromList [(toS (replaceLast "limit" k), restrictRange (readMaybe =<< (toS <$> v)) allRange) | (k,v) <- qParams, isJust v, endingIn ["limit"] k]
offsetParams :: M.HashMap ByteString NonnegRange
offsetParams = M.fromList [(toS (replaceLast "limit" k), maybe allRange rangeGeq (readMaybe =<< (toS <$> v))) | (k,v) <- qParams, isJust v, endingIn ["offset"] k]
urlRange = M.unionWith f limitParams offsetParams
where
f rl ro = Range (BoundaryBelow o) (BoundaryAbove $ o + l - 1)
where
l = fromMaybe 0 $ rangeLimit rl
o = rangeOffset ro
ranges = M.insert "limit" (rangeIntersection headerRange (fromMaybe allRange (M.lookup "limit" urlRange))) urlRange
{-|
Find the best match from a list of content types accepted by the
client in order of decreasing preference and a list of types
producible by the server. If there is no match but the client
accepts */* then return the top server pick.
-}
mutuallyAgreeable :: [ContentType] -> [ContentType] -> Maybe ContentType
mutuallyAgreeable sProduces cAccepts =
let exact = listToMaybe $ L.intersect cAccepts sProduces in
if isNothing exact && CTAny `elem` cAccepts
then listToMaybe sProduces
else exact
-- PRIVATE ---------------------------------------------------------------
{-|
Warning: discards MIME parameters
-}
decodeContentType :: BS.ByteString -> ContentType
decodeContentType ct =
case BS.takeWhile (/= BS.c2w ';') ct of
"application/json" -> CTApplicationJSON
"text/csv" -> CTTextCSV
"application/openapi+json" -> CTOpenAPI
"application/vnd.pgrst.object+json" -> CTSingularJSON
"application/vnd.pgrst.object" -> CTSingularJSON
"application/octet-stream" -> CTOctetStream
"*/*" -> CTAny
ct' -> CTOther ct'
type CsvData = V.Vector (M.HashMap 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.Value
csvToJson (_, vals) =
JSON.Array $ V.map rowToJsonObj vals
where
rowToJsonObj = JSON.Object .
M.map (\str ->
if str == "NULL"
then JSON.Null
else JSON.String $ toS str
)
payloadAttributes :: RequestBody -> JSON.Value -> Maybe PayloadJSON
payloadAttributes raw json =
-- Test that Array contains only Objects having the same keys
case json of
JSON.Array arr ->
case arr V.!? 0 of
Just (JSON.Object o) ->
let canonicalKeys = S.fromList $ M.keys o
areKeysUniform = all (\case
JSON.Object x -> S.fromList (M.keys x) == canonicalKeys
_ -> False) arr in
if areKeysUniform
then Just $ PayloadJSON raw (PJArray $ V.length arr) canonicalKeys
else Nothing
Just _ -> Nothing
Nothing -> Just emptyPJArray
JSON.Object o -> Just $ PayloadJSON raw PJObject (S.fromList $ M.keys o)
-- truncate everything else to an empty array.
_ -> Just emptyPJArray
where
emptyPJArray = PayloadJSON (JSON.encode emptyArray) (PJArray 0) S.empty
+378 -335
View File
@@ -1,362 +1,405 @@
{-# LANGUAGE FlexibleContexts #-}
module PostgREST.App (app, sqlError, isSqlError, contentTypeForAccept) where
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE NamedFieldPuns #-}
import Control.Monad (join)
import Control.Arrow ((***), second)
import Control.Applicative
module PostgREST.App (
postgrest
) where
import Data.Text hiding (map, find)
import Data.Maybe (fromMaybe, mapMaybe, isJust, isNothing)
import Text.Regex.TDFA ((=~))
import Data.Ord (comparing)
import Data.Ranged.Ranges (emptyRange)
import qualified Data.HashMap.Strict as M
import Data.String.Conversions (cs)
import Data.CaseInsensitive (original)
import Data.List (sortBy, find)
import Data.Functor.Identity
import qualified Data.Set as S
import qualified Data.ByteString.Lazy as BL
import qualified Data.ByteString.Char8 as BS
import qualified Blaze.ByteString.Builder as BB
import qualified Data.Csv as CSV
import Control.Applicative
import Data.Aeson as JSON
import qualified Data.ByteString.Char8 as BS
import Data.Maybe
import Data.IORef (IORef, readIORef)
import Data.Text (intercalate)
import Data.Time.Clock (UTCTime)
import qualified Data.Set as S
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 Network.Wai.Parse (parseHttpAccept)
import Network.Wai.Internal (Response(..))
import qualified Hasql.Pool as P
import qualified Hasql.Transaction as HT
import qualified Hasql.Transaction.Sessions as HT
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 Network.HTTP.Types.Header
import Network.HTTP.Types.Status
import Network.HTTP.Types.URI (renderSimpleQuery)
import Network.Wai
import Network.Wai.Middleware.RequestLogger (logStdout)
import PostgREST.Config (AppConfig(..))
import PostgREST.Auth
import PostgREST.PgQuery
import PostgREST.RangeQuery
import PostgREST.PgStructure
import qualified Hasql.Transaction as H
import Prelude
import qualified Data.HashMap.Strict as M
app :: AppConfig -> BL.ByteString -> Request -> H.Tx P.Postgres s Response
app conf reqBody req =
case (path, verb) of
([], _) -> do
body <- encode <$> tables (cs schema)
return $ responseLBS status200 [jsonH] $ cs body
import PostgREST.ApiRequest ( ApiRequest(..), ContentType(..)
, Action(..), Target(..)
, PreferRepresentation (..)
, mutuallyAgreeable
, userApiRequest
)
import PostgREST.Auth (jwtClaims, containsRole, parseSecret)
import PostgREST.Config (AppConfig (..))
import PostgREST.DbStructure
import PostgREST.DbRequestBuilder( readRequest
, mutateRequest
, fieldNames
)
import PostgREST.Error ( simpleError, pgError
, apiRequestError
, singularityError, binaryFieldError
, connectionLostError, gucHeadersError
)
import PostgREST.RangeQuery (allRange, rangeOffset)
import PostgREST.Middleware
import PostgREST.QueryBuilder ( callProc
, requestToQuery
, requestToCountQuery
, createReadStatement
, createWriteStatement
, ResultsWithCount
)
import PostgREST.Types
import PostgREST.OpenAPI
([table], "OPTIONS") -> do
let qt = qualify table
cols <- columns qt
pkey <- map cs <$> primaryKeyColumns qt
return $ responseLBS status200 [jsonH, allOrigins]
$ encode (TableOptions cols pkey)
import Data.Function (id)
import Protolude hiding (intercalate, Proxy)
([table], "GET") ->
if range == Just emptyRange
then return $ responseLBS status416 [] "HTTP Range error"
else do
let qt = qualify table
from = fromMaybe 0 $ rangeOffset <$> range
select = B.Stmt "select " V.empty True <>
parentheticT (
whereT qt qq $ countRows qt
) <> commaq <> (
bodyForAccept contentType qt
. limitT range
. orderT (orderParse qq)
. whereT qt qq
$ selectStar qt
)
row <- H.maybeEx select
let (tableTotal, queryTotal, body) =
fromMaybe (0, 0, Just "" :: Maybe Text) row
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
[contentTypeH, contentRange,
("Content-Location",
"/" <> cs table <>
if Prelude.null canonical then "" else "?" <> cs canonical
)
] (cs $ fromMaybe "[]" body)
postgrest :: AppConfig -> IORef (Maybe DbStructure) -> P.Pool -> IO UTCTime -> IO () -> Application
postgrest conf refDbStructure pool getTime worker =
let middle = (if configQuiet conf then id else logStdout) . defaultMiddle
jwtSecret = parseSecret <$> configJwtSecret conf in
(["postgrest", "users"], "POST") -> do
let user = decode reqBody :: Maybe AuthUser
middle $ \ req respond -> do
time <- getTime
body <- strictRequestBody req
maybeDbStructure <- readIORef refDbStructure
case maybeDbStructure of
Nothing -> respond connectionLostError
Just dbStructure -> do
response <- case userApiRequest (configSchema conf) req body of
Left err -> return $ apiRequestError err
Right apiRequest -> do
eClaims <- jwtClaims jwtSecret (configJwtAudience conf) (toS $ iJWT apiRequest) time (rightToMaybe $ configRoleClaimKey conf)
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))
] ""
let authed = containsRole eClaims
proc = case (iTarget apiRequest, iPayload apiRequest, iPreferSingleObjectParameter apiRequest) of
(TargetProc qi, Just PayloadJSON{pjKeys}, s) -> findProc qi pjKeys s $ dbProcs dbStructure
_ -> Nothing
handleReq = runWithClaims conf eClaims (app dbStructure proc conf) apiRequest
txMode = transactionMode proc (iAction apiRequest)
response <- P.use pool $ HT.transaction HT.ReadCommitted txMode handleReq
return $ either (pgError authed) identity response
when (responseStatus response == status503) worker
respond response
(["postgrest", "tokens"], "POST") ->
case jwtSecret of
"secret" -> return $ responseLBS status500 [jsonH] $
encode . object $ [("message", String "JWT Secret is set as \"secret\" which is an unsafe default.")]
_ -> do
let user = decode reqBody :: Maybe AuthUser
findProc :: QualifiedIdentifier -> S.Set Text -> Bool -> M.HashMap Text [ProcDescription] -> Maybe ProcDescription
findProc qi payloadKeys paramsAsSingleObject allProcs =
let procs = M.lookup (qiName qi) allProcs in
-- Handle overloaded functions case
join $ (case length <$> procs of
Just 1 -> headMay -- if it's not an overloaded function then immediatly get the ProcDescription
_ -> find (\x ->
if paramsAsSingleObject
then length (pdArgs x) == 1 -- if the arg is not of json type let the db give the err
else payloadKeys `S.isSubsetOf` S.fromList (pgaName <$> pdArgs x))
) <$> procs
case user of
Nothing -> return $ responseLBS status400 [jsonH] $
encode . object $ [("message", String "Failed to parse user.")]
Just u -> do
setRole authenticator
login <- signInRole (cs $ userId u)
(cs $ userPass u)
case login of
LoginSuccess role uid ->
return $ responseLBS status201 [ jsonH ] $
encode . object $ [("token", String $ tokenJWT jwtSecret uid role)]
_ -> return $ responseLBS status401 [jsonH] $
encode . object $ [("message", String "Failed authentication.")]
transactionMode :: Maybe ProcDescription -> Action -> HT.Mode
transactionMode proc action =
case action of
ActionRead -> HT.Read
ActionInfo -> HT.Read
ActionInspect -> HT.Read
ActionInvoke{isReadOnly=False} ->
let v = maybe Volatile pdVolatility proc in
if v == Stable || v == Immutable
then HT.Read
else HT.Write
ActionInvoke{isReadOnly=True} -> HT.Read
_ -> HT.Write
([table], "POST") -> do
let qt = qualify table
echoRequested = lookupHeader "Prefer" == Just "return=representation"
parsed :: Either String (V.Vector Text, V.Vector (V.Vector Value))
parsed = if lookupHeader "Content-Type" == Just csvMT
app :: DbStructure -> Maybe ProcDescription -> AppConfig -> ApiRequest -> H.Transaction Response
app dbStructure proc conf apiRequest =
case responseContentTypeOrError (iAccepts apiRequest) (iAction apiRequest) of
Left errorResponse -> return errorResponse
Right contentType ->
case (iAction apiRequest, iTarget apiRequest, iPayload apiRequest) of
(ActionRead, TargetIdent qi, Nothing) ->
let partsField = (,) <$> readSqlParts
<*> (binaryField contentType =<< fldNames) in
case partsField of
Left errorResponse -> return errorResponse
Right ((q, cq), bField) -> do
let stm = createReadStatement q cq (contentType == CTSingularJSON) shouldCount
(contentType == CTTextCSV) bField
row <- H.statement () stm
let (tableTotal, queryTotal, _ , body) = row
(status, contentRange) = rangeHeader queryTotal tableTotal
canonical = iCanonicalQS apiRequest
return $
if contentType == CTSingularJSON && queryTotal /= 1
then singularityError (toInteger queryTotal)
else responseLBS status
[toHeader contentType, contentRange,
("Content-Location",
"/" <> toS (qiName qi) <>
if BS.null canonical then "" else "?" <> toS canonical
)
] (toS body)
(ActionCreate, TargetIdent (QualifiedIdentifier tSchema tName), Just PayloadJSON{pjRaw, pjType}) ->
case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse
Right (sq, mq) -> do
let (isSingle, nRows) = case pjType of
PJArray len -> (len == 1, len)
PJObject -> (True, 1)
if contentType == CTSingularJSON
&& not isSingle
&& iPreferRepresentation apiRequest == Full
then return $ singularityError (toInteger nRows)
else do
let pkCols = tablePKCols dbStructure tSchema tName
stm = createWriteStatement sq mq
(contentType == CTSingularJSON) isSingle
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) pkCols
row <- H.statement (toS pjRaw) stm
let (_, _, fs, body) = extractQueryResult row
headers = catMaybes [
if null fs
then Nothing
else Just (hLocation, "/" <> toS tName <> renderLocationFields fs)
, if iPreferRepresentation apiRequest == Full
then Just $ toHeader contentType
else Nothing
, Just . contentRangeH 1 0 $
toInteger <$> if shouldCount then Just nRows else Nothing
, if null pkCols
then Nothing
else (\x -> ("Preference-Applied", show x)) <$> iPreferResolution apiRequest
]
return . responseLBS status201 headers $
if iPreferRepresentation apiRequest == Full
then toS body else ""
(ActionUpdate, TargetIdent (QualifiedIdentifier tSchema tName), Just p@PayloadJSON{pjRaw}) ->
case (mutateSqlParts tSchema tName, pjIsEmpty p, iPreferRepresentation apiRequest == Full) of
(Left errorResponse, _, _) -> return errorResponse
(_, True, True) -> return $ responseLBS status200 [contentRangeH 1 0 Nothing] "[]"
(_, True, False) -> return $ responseLBS status204 [contentRangeH 1 0 Nothing] ""
(Right (sq, mq), _, _) -> do
let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False (contentType == CTTextCSV)
(iPreferRepresentation apiRequest) []
row <- H.statement (toS pjRaw) stm
let (_, queryTotal, _, body) = extractQueryResult row
if contentType == CTSingularJSON
&& queryTotal /= 1
&& iPreferRepresentation apiRequest == Full
then do
HT.condemn
return $ singularityError (toInteger queryTotal)
else do
let r = contentRangeH 0 (toInteger $ queryTotal-1)
(toInteger <$> if shouldCount then Just queryTotal else Nothing)
s = if iPreferRepresentation apiRequest == Full
then status200
else status204
return $ if iPreferRepresentation apiRequest == Full
then responseLBS s [toHeader contentType, r] (toS body)
else responseLBS s [r] ""
(ActionSingleUpsert, TargetIdent (QualifiedIdentifier tSchema tName), Just PayloadJSON{pjRaw, pjType, pjKeys}) ->
case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse
Right (sq, mq) -> do
let isSingle = case pjType of
PJArray len -> len == 1
PJObject -> True
colNames = colName <$> tableCols dbStructure tSchema tName
if topLevelRange /= allRange
then return $ simpleError status400 [] "Range header and limit/offset querystring parameters are not allowed for PUT"
else if not isSingle
then return $ simpleError status400 [] "PUT payload must contain a single row"
else if S.fromList colNames /= pjKeys
then return $ simpleError status400 [] "You must specify all columns in the payload when using PUT"
else do
row <- H.statement (toS pjRaw) $
createWriteStatement sq mq (contentType == CTSingularJSON) False
(contentType == CTTextCSV) (iPreferRepresentation apiRequest) []
let (_, queryTotal, _, body) = extractQueryResult row
-- Makes sure the querystring pk matches the payload pk
-- e.g. PUT /items?id=eq.1 { "id" : 1, .. } is accepted, PUT /items?id=eq.14 { "id" : 2, .. } is rejected
-- If this condition is not satisfied then nothing is inserted, check the WHERE for INSERT in QueryBuilder.hs to see how it's done
if queryTotal /= 1
then do
HT.condemn
return $ simpleError status400 [] "Payload values do not match URL in primary key column(s)"
else
return $ if iPreferRepresentation apiRequest == Full
then responseLBS status200 [toHeader contentType] (toS body)
else responseLBS status204 [] ""
(ActionDelete, TargetIdent (QualifiedIdentifier tSchema tName), Nothing) ->
case mutateSqlParts tSchema tName of
Left errorResponse -> return errorResponse
Right (sq, mq) -> do
let stm = createWriteStatement sq mq
(contentType == CTSingularJSON) False
(contentType == CTTextCSV)
(iPreferRepresentation apiRequest) []
row <- H.statement mempty stm
let (_, queryTotal, _, body) = extractQueryResult row
r = contentRangeH 1 0 $
toInteger <$> if shouldCount then Just queryTotal else Nothing
if contentType == CTSingularJSON
&& queryTotal /= 1
&& iPreferRepresentation apiRequest == Full
then do
HT.condemn
return $ singularityError (toInteger queryTotal)
else
return $ if iPreferRepresentation apiRequest == Full
then responseLBS status200 [toHeader contentType, r] (toS body)
else responseLBS status204 [r] ""
(ActionInfo, TargetIdent (QualifiedIdentifier tSchema tTable), Nothing) ->
let mTable = find (\t -> tableName t == tTable && tableSchema t == tSchema) (dbTables dbStructure) in
case mTable of
Nothing -> return notFound
Just table ->
let acceptH = (hAllow, if tableInsertable table then "GET,POST,PATCH,DELETE" else "GET") in
return $ responseLBS status200 [allOrigins, acceptH] ""
(ActionInvoke _, TargetProc qi, Just PayloadJSON{pjRaw, pjType, pjKeys}) ->
let returnsScalar = case proc of
Just ProcDescription{pdReturnType = (Single (Scalar _))} -> True
_ -> False
rpcBinaryField = if returnsScalar
then Right Nothing
else binaryField contentType =<< fldNames
parts = (,) <$> readSqlParts <*> rpcBinaryField in
case parts of
Left errorResponse -> return errorResponse
Right ((q, cq), bField) -> do
let isObject = case pjType of
PJObject -> True
PJArray _ -> False
singular = contentType == CTSingularJSON
specifiedPgArgs = filter ((`S.member` pjKeys) . pgaName) $ maybe [] pdArgs proc
row <- H.statement (toS pjRaw) $
callProc qi specifiedPgArgs returnsScalar q cq shouldCount
singular (iPreferSingleObjectParameter apiRequest)
(contentType == CTTextCSV)
(contentType == CTOctetStream) bField isObject
(pgVersion dbStructure)
let (tableTotal, queryTotal, body, jsonHeaders) =
fromMaybe (Just 0, 0, "[]", "[]") row
(status, contentRange) = rangeHeader queryTotal tableTotal
decodedHeaders = first toS $ JSON.eitherDecode $ toS jsonHeaders :: Either Text [GucHeader]
case decodedHeaders of
Left _ -> return gucHeadersError
Right hs ->
if singular && queryTotal /= 1
then do
rows <- CSV.decode CSV.NoHeader reqBody
if V.null rows then Left "CSV requires header"
else Right (V.head rows, (V.map $ V.map $ parseCsvCell . cs) (V.tail rows))
else eitherDecode reqBody >>= \val ->
case val of
Object obj -> Right . second V.singleton . V.unzip . V.fromList $
M.toList obj
_ -> Left "Expecting single JSON object or CSV rows"
case parsed of
Left err -> return $ responseLBS status400 [] $
encode . object $ [("message", String $ "Failed to parse JSON payload. " <> cs err)]
Right toBeInserted -> do
rows :: [Identity Text] <- H.listEx $ uncurry (insertInto qt) toBeInserted
let inserted :: [Object] = mapMaybe (decode . cs . runIdentity) rows
primaryKeys <- primaryKeyColumns qt
let responses = flip map inserted $ \obj -> do
let primaries =
if Prelude.null primaryKeys
then obj
else M.filterWithKey (const . (`elem` primaryKeys)) obj
let params = urlEncodeVars
$ map (\t -> (cs $ fst t, cs (paramFilter $ snd t)))
$ sortBy (comparing fst) $ M.toList primaries
responseLBS status201
[ jsonH
, (hLocation, "/" <> cs table <> "?" <> cs params)
] $ if echoRequested then encode obj else ""
return $ multipart status201 responses
HT.condemn
return $ singularityError (toInteger queryTotal)
else return $ responseLBS status ([toHeader contentType, contentRange] ++ toHeaders hs) (toS body)
(["rpc", proc], "POST") -> do
let qi = QualifiedIdentifier schema (cs proc)
exists <- doesProcExist schema proc
if exists
then do
let call = B.Stmt "select " V.empty True <>
asJson (callProc qi $ fromMaybe M.empty (decode reqBody))
body :: Maybe (Identity Text) <- H.maybeEx call
return $ responseLBS status200 [jsonH]
(cs $ fromMaybe "[]" $ runIdentity <$> body)
else return $ responseLBS status404 [] ""
(ActionInspect, TargetRoot, Nothing) -> do
let host = configHost conf
port = toInteger $ configPort conf
proxy = pickProxy $ toS <$> configProxyUri conf
uri Nothing = ("http", host, port, "/")
uri (Just Proxy { proxyScheme = s, proxyHost = h, proxyPort = p, proxyPath = b }) = (s, h, p, b)
uri' = uri proxy
toTableInfo :: [Table] -> [(Table, [Column], [Text])]
toTableInfo = map (\t -> let (s, tn) = (tableSchema t, tableName t) in (t, tableCols dbStructure s tn, tablePKCols dbStructure s tn))
encodeApi ti sd procs = encodeOpenAPI (concat $ M.elems procs) (toTableInfo ti) uri' sd $ dbPrimaryKeys dbStructure
body <- encodeApi <$> H.statement schema accessibleTables <*> H.statement schema schemaDescription <*> H.statement schema accessibleProcs
return $ responseLBS status200 [toHeader CTOpenAPI] $ toS body
-- check that proc exists
-- check that arg names are all specified
-- select * from "1".proc(a := "foo"::undefined) where whereT limit limitT
_ -> return notFound
([table], "PUT") ->
handleJsonObj reqBody $ \obj -> do
let qt = qualify 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 $ M.keys obj
if S.fromList tableCols == S.fromList cols
then do
let vals = M.elems obj
H.unitEx $ iffNotT
(whereT qt qq $ update qt cols vals)
(insertSelect qt cols vals)
return $ responseLBS status204 [ jsonH ] ""
where
notFound = responseLBS status404 [] ""
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
shouldCount = iPreferCount apiRequest
schema = toS $ configSchema conf
topLevelRange = fromMaybe allRange $ M.lookup "limit" $ iRange apiRequest
rangeHeader queryTotal tableTotal =
let lower = rangeOffset topLevelRange
upper = lower + toInteger queryTotal - 1
contentRange = contentRangeH lower upper (toInteger <$> tableTotal)
status = rangeStatus lower upper (toInteger <$> tableTotal)
in (status, contentRange)
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 = qualify table
up = returningStarT
. whereT qt qq
$ update qt (map cs $ M.keys obj) (M.elems obj)
patch = withT up "t" $ B.Stmt
"select count(t), array_to_json(array_agg(row_to_json(t)))::character varying"
V.empty True
row <- H.maybeEx patch
let (queryTotal, body) =
fromMaybe (0 :: Int, Just "" :: Maybe Text) row
r = contentRangeH 0 (queryTotal-1) queryTotal
echoRequested = lookupHeader "Prefer" == Just "return=representation"
s = case () of _ | queryTotal == 0 -> status404
| echoRequested -> status200
| otherwise -> status204
return $ responseLBS s [ jsonH, r ] $ if echoRequested then cs $ fromMaybe "[]" body else ""
([table], "DELETE") -> do
let qt = qualify table
del = countT
. returningStarT
. whereT qt 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 [] ""
readReq = readRequest (configMaxRows conf) (dbRelations dbStructure) proc apiRequest
fldNames = fieldNames <$> readReq
readDbRequest = DbRead <$> readReq
selectQuery = requestToQuery schema False <$> readDbRequest
countQuery = requestToCountQuery schema <$> readDbRequest
readSqlParts = (,) <$> selectQuery <*> countQuery
mutateSqlParts s t =
(,) <$> selectQuery
<*> (requestToQuery schema False . DbMutate <$> (mutateRequest apiRequest t (tablePKCols dbStructure s t) =<< fldNames))
responseContentTypeOrError :: [ContentType] -> Action -> Either Response ContentType
responseContentTypeOrError accepts action = serves contentTypesForRequest accepts
where
path = pathInfo req
verb = requestMethod req
qq = queryString req
qualify = QualifiedIdentifier schema
hdrs = requestHeaders req
lookupHeader = flip lookup hdrs
accept = lookupHeader hAccept
schema = requestedSchema (cs $ configV1Schema conf) accept
authenticator = cs $ configDbUser conf
jwtSecret = cs $ configJwtSecret conf
range = rangeRequested hdrs
allOrigins = ("Access-Control-Allow-Origin", "*") :: Header
contentType = fromMaybe "application/json" $ contentTypeForAccept accept
contentTypeH = (hContentType, contentType)
contentTypesForRequest =
case action of
ActionRead -> [CTApplicationJSON, CTSingularJSON, CTTextCSV, CTOctetStream]
ActionCreate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionUpdate -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionDelete -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
ActionInvoke _ -> [CTApplicationJSON, CTSingularJSON, CTTextCSV, CTOctetStream]
ActionInspect -> [CTOpenAPI, CTApplicationJSON]
ActionInfo -> [CTTextCSV]
ActionSingleUpsert -> [CTApplicationJSON, CTSingularJSON, CTTextCSV]
serves sProduces cAccepts =
case mutuallyAgreeable sProduces cAccepts of
Nothing -> do
let failed = intercalate ", " $ map (toS . toMime) cAccepts
Left $ simpleError status415 [] $
"None of these Content-Types are available: " <> failed
Just ct -> Right ct
sqlError :: t
sqlError = undefined
binaryField :: ContentType -> [FieldName] -> Either Response (Maybe FieldName)
binaryField CTOctetStream fldNames =
if length fldNames == 1 && fieldName /= Just "*"
then Right fieldName
else Left binaryFieldError
where
fieldName = headMay fldNames
binaryField _ _ = Right Nothing
isSqlError :: t
isSqlError = undefined
splitKeyValue :: BS.ByteString -> (BS.ByteString, BS.ByteString)
splitKeyValue kv = (k, BS.tail v)
where (k, v) = BS.break (== '=') kv
rangeStatus :: Int -> Int -> Int -> Status
rangeStatus from to total
| from > total = status416
| (1 + to - from) < total = status206
renderLocationFields :: [BS.ByteString] -> BS.ByteString
renderLocationFields fields =
renderSimpleQuery True $ map splitKeyValue fields
rangeStatus :: Integer -> Integer -> Maybe Integer -> Status
rangeStatus _ _ Nothing = status200
rangeStatus lower upper (Just total)
| lower > total = status416
| (1 + upper - lower) < 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)
)
contentRangeH :: Integer -> Integer -> Maybe Integer -> Header
contentRangeH lower upper total =
("Content-Range", headerValue)
where
headerValue = rangeString <> "/" <> totalString
rangeString
| totalNotZero && fromInRange = show lower <> "-" <> show upper
| otherwise = "*"
totalString = maybe "*" show total
totalNotZero = maybe True (0 /=) total
fromInRange = lower <= upper
requestedSchema :: Text -> Maybe BS.ByteString -> Text
requestedSchema v1schema accept =
case verStr of
Just [[_, ver]] -> if ver == "1" then v1schema else cs ver
_ -> v1schema
where verRegex = "version[ ]*=[ ]*([0-9]+)" :: BS.ByteString
verStr = (=~ verRegex) <$> accept :: Maybe [[BS.ByteString]]
jsonMT :: BS.ByteString
jsonMT = "application/json"
csvMT :: BS.ByteString
csvMT = "text/csv"
allMT :: BS.ByteString
allMT = "*/*"
jsonH :: Header
jsonH = (hContentType, jsonMT)
contentTypeForAccept :: Maybe BS.ByteString -> Maybe BS.ByteString
contentTypeForAccept accept
| isNothing accept || has allMT || has jsonMT = Just jsonMT
| has csvMT = Just csvMT
| otherwise = Nothing
where
Just acceptH = accept
findInAccept = flip find $ parseHttpAccept acceptH
has = isJust . findInAccept . BS.isPrefixOf
bodyForAccept :: BS.ByteString -> QualifiedIdentifier -> StatementT
bodyForAccept contentType table
| contentType == csvMT = asCsvWithCount table
| otherwise = asJsonWithCount -- defaults to 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")]
parseCsvCell :: BL.ByteString -> Value
parseCsvCell s = if s == "NULL" then Null else String $ cs s
multipart :: Status -> [Response] -> Response
multipart _ [] = responseLBS status204 [] ""
multipart _ [r] = r
multipart s rs =
responseLBS s [(hContentType, "multipart/mixed; boundary=\"postgrest_boundary\"")] $
BL.intercalate "\n--postgrest_boundary\n" (map renderResponseBody rs)
where
renderHeader :: Header -> BL.ByteString
renderHeader (k, v) = cs (original k) <> ": " <> cs v
renderResponseBody :: Response -> BL.ByteString
renderResponseBody (ResponseBuilder _ headers b) =
BL.intercalate "\n" (map renderHeader headers)
<> "\n\n" <> BB.toLazyByteString b
renderResponseBody _ = error
"Unable to create multipart response from non-ResponseBuilder"
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, [], "")
+99 -89
View File
@@ -1,101 +1,111 @@
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables, OverloadedStrings #-}
module PostgREST.Auth where
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE LambdaCase #-}
{-|
Module : PostgREST.Auth
Description : PostgREST authorization functions.
import Data.Aeson
import Control.Monad (mzero)
import Control.Applicative
import Crypto.BCrypt
import Data.Text
import Data.Monoid
import Data.Map
import qualified Data.Vector as V
import qualified Hasql as H
import qualified Hasql.Backend as B
import qualified Hasql.Postgres as P
import qualified Web.JWT as JWT
import Data.String.Conversions (cs)
import PostgREST.PgQuery (pgFmtLit)
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.
import Prelude
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 (
containsRole
, jwtClaims
, JWTAttempt(..)
, parseSecret
) where
import System.IO.Unsafe
import Control.Lens.Operators
import Control.Lens (set)
import qualified Data.Aeson as JSON
import qualified Data.HashMap.Strict as M
import Data.Time.Clock (UTCTime)
import Data.Vector as V
import PostgREST.Types
import Protolude
data AuthUser = AuthUser {
userId :: String
, userPass :: String
, userRole :: String
} deriving (Show)
import qualified Crypto.JOSE.Types as JOSE.Types
import Crypto.JWT
instance FromJSON AuthUser where
parseJSON (Object v) = AuthUser <$>
v .: "id" <*>
v .: "pass" <*>
v .:? "role" .!= ""
parseJSON _ = mzero
{-|
Possible situations encountered with client JWTs
-}
data JWTAttempt = JWTInvalid JWTError
| JWTMissingSecret
| JWTClaims (M.HashMap Text JSON.Value)
deriving (Eq, Show)
instance ToJSON AuthUser where
toJSON u = object [
"id" .= userId u
, "pass" .= userPass u
, "role" .= userRole u ]
{-|
Receives the JWT secret and audience (from config) and a JWT and returns a map
of JWT claims.
-}
jwtClaims :: Maybe JWKSet -> Maybe StringOrURI -> LByteString -> UTCTime -> Maybe JSPath -> IO JWTAttempt
jwtClaims _ _ "" _ _ = return $ JWTClaims M.empty
jwtClaims secret audience payload time jspath =
case secret of
Nothing -> return JWTMissingSecret
Just s -> do
let validation = set allowedSkew 1 $ defaultJWTValidationSettings (maybe (const True) (==) audience)
eJwt <- runExceptT $ do
jwt <- decodeCompact payload
verifyClaimsAt validation s time jwt
return $ case eJwt of
Left e -> JWTInvalid e
Right jwt -> JWTClaims $ claims2map jwt jspath
type DbRole = Text
type UserId = Text
{-|
Turn JWT ClaimSet into something easier to work with,
also here the jspath is applied to put the "role" in the map
-}
claims2map :: ClaimsSet -> Maybe JSPath -> M.HashMap Text JSON.Value
claims2map claims jspath = (\case
val@(JSON.Object o) ->
let role = maybe M.empty (M.singleton "role") $
walkJSPath (Just val) =<< jspath in
M.delete "role" o `M.union` role -- mutating the map
_ -> M.empty
) $ JSON.toJSON claims
data LoginAttempt =
NoCredentials
| MalformedAuth
| LoginFailed
| LoginSuccess DbRole UserId
deriving (Eq, Show)
walkJSPath :: Maybe JSON.Value -> JSPath -> Maybe JSON.Value
walkJSPath x [] = x
walkJSPath (Just (JSON.Object o)) (JSPKey key:rest) = walkJSPath (M.lookup key o) rest
walkJSPath (Just (JSON.Array ar)) (JSPIdx idx:rest) = walkJSPath (ar V.!? idx) rest
walkJSPath _ _ = Nothing
checkPass :: Text -> Text -> Bool
checkPass = (. cs) . validatePassword . cs
{-|
Whether a response from jwtClaims contains a role claim
-}
containsRole :: JWTAttempt -> Bool
containsRole (JWTClaims claims) = M.member "role" claims
containsRole _ = False
setRole :: Text -> H.Tx P.Postgres s ()
setRole role = H.unitEx $ B.Stmt ("set local role " <> cs (pgFmtLit role)) V.empty True
{-|
Parse `jwt-secret` configuration option and turn into a JWKSet.
setUserId :: Text -> H.Tx P.Postgres s ()
setUserId uid = if uid /= "" then
H.unitEx $ B.Stmt ("set local user_vars.user_id = " <> cs (pgFmtLit uid)) V.empty True
else
resetUserId
There are three ways to specify `jwt-secret`: text secret, JSON Web Key
(JWK), or JSON Web Key Set (JWKS). The first two are converted into a JWKSet
with one key and the last is converted as is.
-}
parseSecret :: ByteString -> JWKSet
parseSecret str =
fromMaybe (maybe secret (\jwk' -> JWKSet [jwk']) maybeJWK)
maybeJWKSet
where
maybeJWKSet = JSON.decode (toS str) :: Maybe JWKSet
maybeJWK = JSON.decode (toS str) :: Maybe JWK
secret = JWKSet [jwkFromSecret str]
resetUserId :: H.Tx P.Postgres s ()
resetUserId = H.unitEx [H.stmt|reset user_vars.user_id|]
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 id, pass, rolname from postgrest.auth where id = ?|] user
return $ maybe LoginFailed (\r ->
let (uid, hashed, role) = r in
if checkPass hashed pass
then LoginSuccess role uid
else LoginFailed
) u
signInWithJWT :: Text -> Text -> LoginAttempt
signInWithJWT secret input = case maybeRole of
Just (Just (String role)) -> case maybeUserId of
Just (Just (String uid)) -> LoginSuccess (cs role) (cs uid)
_ -> LoginFailed
_ -> LoginFailed
where
maybeRole = (Data.Map.lookup "role" <$> claims) ::Maybe (Maybe Value)
maybeUserId = (Data.Map.lookup "id" <$> claims) ::Maybe (Maybe Value)
claims = JWT.unregisteredClaims <$> JWT.claims <$> decoded
decoded = JWT.decodeAndVerifySignature (JWT.secret secret) input
tokenJWT :: Text -> Text -> Text -> Text
tokenJWT secret uid role = JWT.encodeSigned JWT.HS256 (JWT.secret secret) claimsSet
where
claimsSet = JWT.def {
JWT.unregisteredClaims = Data.Map.fromList [("id", String uid), ("role", String role)]
}
{-|
Internal helper to generate a symmetric HMAC-SHA256 JWK from a text secret.
-}
jwkFromSecret :: ByteString -> JWK
jwkFromSecret key =
fromKeyMaterial km
& jwkUse ?~ Sig
& jwkAlg ?~ JWSAlg HS256
where
km = OctKeyMaterial (OctKeyParameters (JOSE.Types.Base64Octets key))
+219 -42
View File
@@ -1,64 +1,241 @@
module PostgREST.Config where
{-# LANGUAGE LambdaCase, TemplateHaskell #-}
{-# OPTIONS_GHC -fno-warn-type-defaults #-}
{-|
Module : PostgREST.Config
Description : Manages PostgREST configuration options.
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(..))
import Prelude
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
, docsVersion
, readOptions
, corsPolicy
, AppConfig (..)
)
where
import Control.Applicative
import Control.Monad (fail)
import Control.Lens (preview)
import Crypto.JWT (StringOrURI,
stringOrUri)
import qualified Data.ByteString as B
import qualified Data.ByteString.Char8 as BS
import qualified Data.CaseInsensitive as CI
import qualified Data.Configurator as C
import qualified Data.Configurator.Parser as C
import Data.Configurator.Types as C
import Data.List (lookup)
import Data.Monoid
import Data.Scientific (floatingOrInteger)
import Data.String (String)
import Data.Text (dropAround,
intercalate, lines,
strip, take, splitOn)
import Data.Text.Encoding (encodeUtf8)
import Data.Text.IO (hPutStrLn)
import Data.Version (versionBranch)
import Development.GitRev (gitHash)
import Network.Wai
import Network.Wai.Middleware.Cors (CorsResourcePolicy (..))
import Options.Applicative hiding (str)
import Paths_postgrest (version)
import PostgREST.Parsers (pRoleClaimKey)
import PostgREST.Types (ApiRequestError(..),
JSPath, JSPathExp(..))
import Protolude hiding (hPutStrLn, take,
intercalate, (<>))
import System.IO (hPrint)
import System.IO.Error (IOError)
import Text.Heredoc
import Text.PrettyPrint.ANSI.Leijen hiding ((<$>), (<>))
import qualified Text.PrettyPrint.ANSI.Leijen as L
-- | Config file settings for the server
data AppConfig = AppConfig {
configDbName :: String
, configDbPort :: Int
, configDbUser :: String
, configDbPass :: String
, configDbHost :: String
configDatabase :: Text
, configAnonRole :: Text
, configProxyUri :: Maybe Text
, configSchema :: Text
, configHost :: Text
, configPort :: Int
, configPort :: Int
, configAnonRole :: String
, configSecure :: Bool
, configPool :: Int
, configV1Schema :: String
, configJwtSecret :: String
, configJwtSecret :: Maybe B.ByteString
, configJwtSecretIsBase64 :: Bool
, configJwtAudience :: Maybe StringOrURI
, configPool :: Int
, configMaxRows :: Maybe Integer
, configReqCheck :: Maybe Text
, configQuiet :: Bool
, configSettings :: [(Text, Text)]
, configRoleClaimKey :: Either ApiRequestError JSPath
, configExtraSearchPath :: [Text]
}
argParser :: Parser AppConfig
argParser = AppConfig
<$> strOption (long "db-name" <> short 'd' <> metavar "NAME" <> help "name of database")
<*> option auto (long "db-port" <> short 'P' <> metavar "PORT" <> value 5432 <> help "postgres server port" <> showDefault)
<*> strOption (long "db-user" <> short 'U' <> metavar "ROLE" <> help "postgres authenticator role")
<*> strOption (long "db-pass" <> metavar "PASS" <> value "" <> help "password for authenticator role")
<*> strOption (long "db-host" <> metavar "HOST" <> value "localhost" <> help "postgres server hostname" <> showDefault)
<*> option auto (long "port" <> short 'p' <> metavar "PORT" <> value 3000 <> help "port number on which to run HTTP server" <> showDefault)
<*> strOption (long "anonymous" <> short 'a' <> metavar "ROLE" <> help "postgres role to use for non-authenticated requests")
<*> switch (long "secure" <> short 's' <> help "Redirect all requests to HTTPS")
<*> option auto (long "db-pool" <> metavar "COUNT" <> value 10 <> help "Max connections in database pool" <> showDefault)
<*> strOption (long "v1schema" <> metavar "NAME" <> value "1" <> help "Schema to use for nonspecified version (or explicit v1)" <> showDefault)
<*> strOption (long "jwt-secret" <> metavar "SECRET" <> value "secret" <> help "Secret used to encrypt and decrypt JWT tokens)" <> showDefault)
defaultCorsPolicy :: CorsResourcePolicy
defaultCorsPolicy = CorsResourcePolicy Nothing
["GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"] ["Authorization"] Nothing
["GET", "POST", "PATCH", "PUT", "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"
]
"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
Just hdrs -> map (CI.mk . toS . strip . toS) $ BS.split ',' hdrs
Nothing -> []
-- | User friendly version number
prettyVersion :: Text
prettyVersion =
intercalate "." (map show $ versionBranch version)
<> " (" <> take 7 $(gitHash) <> ")"
-- | Version number used in docs
docsVersion :: Text
docsVersion = "v" <> dropAround (== '.') (dropAround (/= '.') prettyVersion)
-- | Function to read and parse options from the command line
readOptions :: IO AppConfig
readOptions = do
-- First read the config file path from command line
cfgPath <- customExecParser parserPrefs opts
-- Now read the actual config file
conf <- catch
(C.readConfig =<< C.load [C.Required cfgPath])
configNotfoundHint
let (mAppConf, errs) = flip C.runParserM conf $
AppConfig
<$> C.key "db-uri"
<*> C.key "db-anon-role"
<*> (mfilter (/= "") <$> C.key "server-proxy-uri")
<*> C.key "db-schema"
<*> (fromMaybe "127.0.0.1" . mfilter (/= "") <$> C.key "server-host")
<*> (fromMaybe 3000 . join . fmap coerceInt <$> C.key "server-port")
<*> (fmap encodeUtf8 . mfilter (/= "") <$> C.key "jwt-secret")
<*> (fromMaybe False . join . fmap coerceBool <$> C.key "secret-is-base64")
<*> parseJwtAudience "jwt-aud"
<*> (fromMaybe 10 . join . fmap coerceInt <$> C.key "db-pool")
<*> (join . fmap coerceInt <$> C.key "max-rows")
<*> (mfilter (/= "") <$> C.key "pre-request")
<*> pure False
<*> (fmap (fmap coerceText) <$> C.subassocs "app.settings")
<*> (maybe (Right [JSPKey "role"]) parseRoleClaimKey <$> C.key "role-claim-key")
<*> (maybe ["public"] splitExtraSearchPath <$> C.key "db-extra-search-path")
case mAppConf of
Nothing -> do
forM_ errs $ hPrint stderr
exitFailure
Just appConf ->
return appConf
where
parseJwtAudience :: Name -> C.ConfigParserM (Maybe StringOrURI)
parseJwtAudience k =
C.key k >>= \case
Nothing -> pure Nothing -- no audience in config file
Just aud -> case preview stringOrUri (aud :: String) of
Nothing -> fail "Invalid Jwt audience. Check your configuration."
(Just "") -> pure Nothing
aud' -> pure aud'
coerceText :: Value -> Text
coerceText (String s) = s
coerceText v = show v
coerceInt :: (Read i, Integral i) => Value -> Maybe i
coerceInt (Number x) = rightToMaybe $ floatingOrInteger x
coerceInt (String x) = readMaybe $ toS x
coerceInt _ = Nothing
coerceBool :: Value -> Maybe Bool
coerceBool (Bool b) = Just b
coerceBool (String b) = readMaybe $ toS b
coerceBool _ = Nothing
parseRoleClaimKey :: Value -> Either ApiRequestError JSPath
parseRoleClaimKey (String s) = pRoleClaimKey s
parseRoleClaimKey v = pRoleClaimKey $ show v
splitExtraSearchPath :: Value -> [Text]
splitExtraSearchPath (String s) = strip <$> splitOn "," s
splitExtraSearchPath _ = []
opts = info (helper <*> pathParser) $
fullDesc
<> progDesc (
"PostgREST "
<> toS prettyVersion
<> " / create a REST API to an existing Postgres database"
)
<> footerDoc (Just $
text "Example Config File:"
L.<> nest 2 (hardline L.<> exampleCfg)
)
parserPrefs = prefs showHelpOnError
configNotfoundHint :: IOError -> IO a
configNotfoundHint e = do
hPutStrLn stderr $
"Cannot open config file:\n\t" <> show e
exitFailure
exampleCfg :: Doc
exampleCfg = vsep . map (text . toS) . lines $
[str|db-uri = "postgres://user:pass@localhost:5432/dbname"
|db-schema = "public" # this schema gets added to the search_path of every request
|db-anon-role = "postgres"
|db-pool = 10
|
|server-host = "127.0.0.1"
|server-port = 3000
|
|## base url for swagger output
|# server-proxy-uri = ""
|
|## choose a secret, JSON Web Key (or set) to enable JWT auth
|## (use "@filename" to load from separate file)
|# jwt-secret = "foo"
|# secret-is-base64 = false
|# jwt-aud = "your_audience_claim"
|
|## limit rows in response
|# max-rows = 1000
|
|## stored proc to exec immediately after auth
|# pre-request = "stored_proc_name"
|
|## jspath to the role claim key
|# role-claim-key = ".role"
|
|## extra schemas to add to the search_path of every request
|# db-extra-search-path = "extensions, util"
|]
pathParser :: Parser FilePath
pathParser =
strArgument $
metavar "FILENAME" <>
help "Path to configuration file"
+342
View File
@@ -0,0 +1,342 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DuplicateRecordFields#-}
{-# LANGUAGE LambdaCase #-}
{-# LANGUAGE MultiWayIf #-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.DbRequestBuilder (
readRequest
, mutateRequest
, fieldNames
) where
import Control.Applicative
import Control.Arrow ((***))
import Control.Lens.Getter (view)
import Control.Lens.Tuple (_1)
import qualified Data.ByteString.Char8 as BS
import Data.List (delete)
import Data.Maybe (fromJust)
import qualified Data.Set as S
import Data.Text (isInfixOf)
import Data.Tree
import Data.Either.Combinators (mapLeft)
import Network.Wai
import Data.Foldable (foldr1)
import qualified Data.HashMap.Strict as M
import PostgREST.ApiRequest ( ApiRequest(..)
, PreferRepresentation(..)
, Action(..), Target(..)
, PreferRepresentation (..)
)
import PostgREST.Error (apiRequestError)
import PostgREST.Parsers
import PostgREST.RangeQuery (NonnegRange, restrictRange, allRange)
import PostgREST.Types
import Protolude hiding (from)
import Text.Regex.TDFA ((=~))
import Unsafe (unsafeHead)
readRequest :: Maybe Integer -> [Relation] -> Maybe ProcDescription -> ApiRequest -> Either Response ReadRequest
readRequest maxRows allRels proc apiRequest =
mapLeft apiRequestError $
treeRestrictRange maxRows =<<
augumentRequestWithJoin schema relations =<<
addFiltersOrdersRanges apiRequest <*>
(buildReadRequest <$> pRequestSelect (iSelect apiRequest))
where
action = iAction apiRequest
(schema, rootTableName) = fromJust $ -- Make it safe
let target = iTarget apiRequest in
case target of
(TargetIdent (QualifiedIdentifier s t) ) -> Just (s, t)
(TargetProc (QualifiedIdentifier s pName) ) -> Just (s, tName)
where
tName = case pdReturnType <$> proc of
Just (SetOf (Composite qi)) -> qiName qi
Just (Single (Composite qi)) -> qiName qi
_ -> pName
_ -> Nothing
-- Build tree with a Depth attribute so when a self join occurs we can differentiate the parent and child tables by having
-- an alias like "table_depth", this is related to issue #987.
buildReadRequest :: [Tree SelectItem] -> ReadRequest
buildReadRequest fieldTree =
let rootDepth = 0
rootNodeName = if action == ActionRead then rootTableName else sourceCTEName in
foldr (treeEntry rootDepth) (Node (Select [] [rootNodeName] [] [] [] allRange, (rootNodeName, Nothing, Nothing, Nothing, rootDepth)) []) fieldTree
where
treeEntry :: Depth -> Tree SelectItem -> ReadRequest -> ReadRequest
treeEntry depth (Node fld@((fn, _),_,alias,relationDetail) fldForest) (Node (q, i) rForest) =
let nxtDepth = succ depth in
case fldForest of
[] -> Node (q {select=fld:select q}, i) rForest
_ -> Node (q, i) $
foldr (treeEntry nxtDepth) (Node (Select [] [fn] [] [] [] allRange, (fn, Nothing, alias, relationDetail, nxtDepth)) []) fldForest:rForest
relations :: [Relation]
relations = case action of
ActionCreate -> fakeSourceRelations ++ allRels
ActionUpdate -> fakeSourceRelations ++ allRels
ActionDelete -> fakeSourceRelations ++ allRels
ActionInvoke _ -> fakeSourceRelations ++ allRels
_ -> allRels
where fakeSourceRelations = mapMaybe (toSourceRelation rootTableName) allRels
-- in a relation where one of the tables matches "TableName"
-- replace the name to that table with pg_source
-- this "fake" relations is needed so that in a mutate query
-- we can look at 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 {relLinkTable=(\tbl -> tbl {tableName=sourceCTEName}) <$> rt}
| otherwise = Nothing
treeRestrictRange :: Maybe Integer -> ReadRequest -> Either ApiRequestError ReadRequest
treeRestrictRange maxRows_ request = pure $ nodeRestrictRange maxRows_ `fmap` request
where
nodeRestrictRange :: Maybe Integer -> ReadNode -> ReadNode
nodeRestrictRange m (q@Select {range_=r}, i) = (q{range_=restrictRange m r }, i)
augumentRequestWithJoin :: Schema -> [Relation] -> ReadRequest -> Either ApiRequestError ReadRequest
augumentRequestWithJoin schema allRels request =
addRelations schema allRels Nothing request
>>= addJoinConditions schema
addRelations :: Schema -> [Relation] -> Maybe ReadRequest -> ReadRequest -> Either ApiRequestError ReadRequest
addRelations schema allRelations parentNode (Node (query, (nodeName, _, alias, relationDetail, depth)) forest) =
case parentNode of
Just (Node (Select{from=[parentNodeTable]}, _) _) ->
let newFrom r = (\tName -> if tName == nodeName then tableName (relTable r) else tName) <$> from query
newReadNode = (\r -> (query{from=newFrom r}, (nodeName, Just r, alias, Nothing, depth))) <$> rel
rel :: Either ApiRequestError Relation
rel = note (NoRelationBetween parentNodeTable nodeName) $
findRelation schema allRelations nodeName parentNodeTable relationDetail in
Node <$> newReadNode <*> (updateForest . hush $ Node <$> newReadNode <*> pure forest)
_ ->
let rn = (query, (nodeName, Just r, alias, Nothing, depth))
r = Relation t [] t [] Root Nothing Nothing Nothing
t = Table schema nodeName Nothing True in -- !!! TODO find another way to get the table from the query
Node rn <$> updateForest (Just $ Node rn forest)
where
updateForest :: Maybe ReadRequest -> Either ApiRequestError [ReadRequest]
updateForest rq = mapM (addRelations schema allRelations rq) forest
findRelation :: Schema -> [Relation] -> NodeName -> TableName -> Maybe RelationDetail -> Maybe Relation
findRelation schema allRelations nodeTableName parentNodeTableName relationDetail =
find (\Relation{relTable, relColumns, relFTable, relFColumns, relType, relLinkTable} ->
-- Both relation ends need to be on the exposed schema
schema == tableSchema relTable && schema == tableSchema relFTable &&
case relationDetail of
Nothing ->
-- (request) => projects { ..., clients{...} }
-- will match
-- (relation type) => parent
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
(
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable -- match relation foreign table name
) ||
-- (request) => projects { ..., client_id{...} }
-- will match
-- (relation type) => parent
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
(
parentNodeTableName == tableName relFTable &&
length relFColumns == 1 &&
-- match common foreign key names(table_name_id, table_name_fk) to table_name
(toS ("^" <> colName (unsafeHead relFColumns) <> "_?(?:|[iI][dD]|[fF][kK])$") :: BS.ByteString) =~ (toS nodeTableName :: BS.ByteString)
)
-- (request) => project_id { ..., client_id{...} }
-- will match
-- (relation type) => parent
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
-- this case works becasue before reaching this place
-- addRelation will turn project_id to project so the above condition will match
Just rd ->
-- (request) => clients { ..., projects.client_id{...} }
-- will match
-- (relation type) => child
-- (entity) => clients {id}
-- (foriegn entity) => projects {client_id}
(
relType == Child &&
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable && -- match relation foreign table name
length relColumns == 1 &&
rd == colName (unsafeHead relColumns)
) ||
-- (request) => message { ..., person_detail.sender{...} }
-- will match
-- (relation type) => parent
-- (entity) => message {sender}
-- (foriegn entity) => person_detail {id}
(
relType == Parent &&
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable && -- match relation foreign table name
length relFColumns == 1 &&
rd == colName (unsafeHead relFColumns)
) ||
-- (request) => tasks { ..., users.tasks_users{...} }
-- will match
-- (relation type) => many
-- (entity) => users
-- (foriegn entity) => tasks
(
relType == Many &&
nodeTableName == tableName relTable && -- match relation table name
parentNodeTableName == tableName relFTable && -- match relation foreign table name
rd == tableName (fromJust relLinkTable)
)
) allRelations
addJoinConditions :: Schema -> ReadRequest -> Either ApiRequestError ReadRequest
addJoinConditions schema (Node node@(query, nodeProps@(_, relation, _, _, _)) forest) =
case relation of
Just Relation{relType=Root} -> Node node <$> updatedForest -- this is the root node
Just rel@Relation{relType=Parent} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=Child} -> Node (augmentQuery rel, nodeProps) <$> updatedForest
Just rel@Relation{relType=Many, relLinkTable=(Just linkTable)} ->
let rq = augmentQuery rel in
Node (rq{from=tableName linkTable:from rq}, nodeProps) <$> updatedForest
_ -> Left UnknownRelation
where
updatedForest = mapM (addJoinConditions schema) forest
augmentQuery rel = foldr addJoinCond query (getJoinConditions rel)
addJoinCond :: JoinCondition -> ReadQuery -> ReadQuery
addJoinCond jc rq@Select{joinConditions=jcs} = rq{joinConditions=jc:jcs}
getJoinConditions :: Relation -> [JoinCondition]
getJoinConditions (Relation Table{tableSchema=tSchema, tableName=tN} cols Table{tableName=ftN} fCols typ lt lc1 lc2) =
if | typ == Child || typ == Parent ->
zipWith (toJoinCondition tN ftN) cols fCols
| typ == Many ->
let ltN = maybe "" tableName lt in
zipWith (toJoinCondition tN ltN) cols (fromMaybe [] lc1) ++ zipWith (toJoinCondition ftN ltN) fCols (fromMaybe [] lc2)
| typ == Root -> witness
where
toJoinCondition :: Text -> Text -> Column -> Column -> JoinCondition
toJoinCondition tb ftb c fc =
JoinCondition (QualifiedIdentifier tSchema tb, Nothing, colName c)
(QualifiedIdentifier tSchema ftb, Nothing, colName fc)
addFiltersOrdersRanges :: ApiRequest -> Either ApiRequestError (ReadRequest -> ReadRequest)
addFiltersOrdersRanges apiRequest = foldr1 (liftA2 (.)) [
flip (foldr addFilter) <$> filters,
flip (foldr addOrder) <$> orders,
flip (foldr addRange) <$> ranges,
flip (foldr addLogicTree) <$> logicForest
]
{-
The esence of what is going on above is that we are composing tree functions
of type (ReadRequest->ReadRequest) that are in (Either ParseError a) context
-}
where
filters :: Either ApiRequestError [(EmbedPath, Filter)]
filters = mapM pRequestFilter flts
logicForest :: Either ApiRequestError [(EmbedPath, LogicTree)]
logicForest = mapM pRequestLogicTree logFrst
action = iAction apiRequest
-- there can be no filters on the root table when we are doing insert/update/delete
(flts, logFrst) =
case action of
ActionInvoke _ -> (iFilters apiRequest, iLogic apiRequest)
ActionRead -> (iFilters apiRequest, iLogic apiRequest)
_ -> join (***) (filter (( "." `isInfixOf` ) . fst)) (iFilters apiRequest, iLogic apiRequest)
orders :: Either ApiRequestError [(EmbedPath, [OrderTerm])]
orders = mapM pRequestOrder $ iOrder apiRequest
ranges :: Either ApiRequestError [(EmbedPath, NonnegRange)]
ranges = mapM pRequestRange $ M.toList $ iRange apiRequest
addFilterToNode :: Filter -> ReadRequest -> ReadRequest
addFilterToNode flt (Node (q@Select {where_=lf}, i) f) = Node (q{where_=addFilterToLogicForest flt lf}::ReadQuery, i) f
addFilter :: (EmbedPath, Filter) -> ReadRequest -> ReadRequest
addFilter = addProperty addFilterToNode
addOrderToNode :: [OrderTerm] -> ReadRequest -> ReadRequest
addOrderToNode o (Node (q,i) f) = Node (q{order=o}, i) f
addOrder :: (EmbedPath, [OrderTerm]) -> ReadRequest -> ReadRequest
addOrder = addProperty addOrderToNode
addRangeToNode :: NonnegRange -> ReadRequest -> ReadRequest
addRangeToNode r (Node (q,i) f) = Node (q{range_=r}, i) f
addRange :: (EmbedPath, NonnegRange) -> ReadRequest -> ReadRequest
addRange = addProperty addRangeToNode
addLogicTreeToNode :: LogicTree -> ReadRequest -> ReadRequest
addLogicTreeToNode t (Node (q@Select{where_=lf},i) f) = Node (q{where_=t:lf}::ReadQuery, i) f
addLogicTree :: (EmbedPath, LogicTree) -> ReadRequest -> ReadRequest
addLogicTree = addProperty addLogicTreeToNode
addProperty :: (a -> ReadRequest -> ReadRequest) -> (EmbedPath, a) -> ReadRequest -> ReadRequest
addProperty f ([], a) rr = f a rr
addProperty f (targetNodeName:remainingPath, a) (Node rn forest) =
case pathNode of
Nothing -> Node rn forest -- the property is silenty dropped in the Request does not contain the required path
Just tn -> Node rn (addProperty f (remainingPath, a) tn:delete tn forest)
where
pathNode = find (\(Node (_,(nodeName,_,alias,_,_)) _) -> nodeName == targetNodeName || alias == Just targetNodeName) forest
mutateRequest :: ApiRequest -> TableName -> [Text] -> [FieldName] -> Either Response MutateRequest
mutateRequest apiRequest tName pkCols fldNames = mapLeft apiRequestError $
case action of
ActionCreate -> Right $ Insert tName pkCols payload (iPreferResolution apiRequest) [] returnings
ActionUpdate -> Update tName payload <$> combinedLogic <*> pure returnings
ActionSingleUpsert ->
(\flts ->
if null (iLogic apiRequest) &&
S.fromList (fst <$> iFilters apiRequest) == S.fromList pkCols &&
not (null (S.fromList pkCols)) &&
all (\case
Filter _ (OpExpr False (Op "eq" _)) -> True
_ -> False) flts
then Insert tName pkCols payload (Just MergeDuplicates) <$> combinedLogic <*> pure returnings
else
Left InvalidFilters) =<< filters
ActionDelete -> Delete tName <$> combinedLogic <*> pure returnings
_ -> Left UnsupportedVerb
where
action = iAction apiRequest
payload = fromJust $ iPayload apiRequest
returnings = if iPreferRepresentation apiRequest == None then [] else fldNames
filters = map snd <$> mapM pRequestFilter mutateFilters
logic = map snd <$> mapM pRequestLogicTree logicFilters
combinedLogic = foldr addFilterToLogicForest <$> logic <*> filters
-- update/delete filters can be only on the root table
(mutateFilters, logicFilters) = join (***) onlyRoot (iFilters apiRequest, iLogic apiRequest)
onlyRoot = filter (not . ( "." `isInfixOf` ) . fst)
fieldNames :: ReadRequest -> [FieldName]
fieldNames (Node (sel, _) forest) =
map (fst . view _1) (select sel) ++ map colName fks
where
fks = concatMap (fromMaybe [] . f) forest
f (Node (_, (_, Just Relation{relFColumns=cols, relType=Parent}, _, _, _)) _) = Just cols
f _ = Nothing
-- Traditional filters(e.g. id=eq.1) are added as root nodes of the LogicTree
-- they are later concatenated with AND in the QueryBuilder
addFilterToLogicForest :: Filter -> [LogicTree] -> [LogicTree]
addFilterToLogicForest flt lf = Stmnt flt : lf
+771
View File
@@ -0,0 +1,771 @@
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE MultiParamTypeClasses #-}
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE TypeSynonymInstances #-}
{-# LANGUAGE NamedFieldPuns #-}
module PostgREST.DbStructure (
getDbStructure
, accessibleTables
, accessibleProcs
, schemaDescription
, getPgVersion
) where
import qualified Hasql.Decoders as HD
import qualified Hasql.Encoders as HE
import qualified Hasql.Statement as H
import Control.Applicative
import qualified Data.HashMap.Strict as M
import qualified Data.List as L
import Data.Set as S (fromList)
import Data.Text (split, strip,
breakOn, dropAround,
splitOn)
import qualified Data.Text as T
import qualified Hasql.Session as H
import qualified Hasql.Transaction as HT
import PostgREST.Types
import Text.InterpolatedString.Perl6 (q, qc)
import GHC.Exts (groupWith)
import Protolude
import Unsafe (unsafeHead)
getDbStructure :: Schema -> PgVersion -> HT.Transaction DbStructure
getDbStructure schema pgVer = do
HT.sql "set local schema ''" -- for getting the fully qualified name(schema.name) of every db object
tabs <- HT.statement () allTables
cols <- HT.statement schema $ allColumns tabs
syns <- HT.statement schema $ allSynonyms cols pgVer
childRels <- HT.statement () $ allChildRelations tabs cols
keys <- HT.statement () $ allPrimaryKeys tabs
procs <- HT.statement schema allProcs
let rels = addManyToManyRelations . addParentRelations $ addViewChildRelations syns childRels
cols' = addForeignKeys rels cols
keys' = addViewPrimaryKeys syns keys
return DbStructure {
dbTables = tabs
, dbColumns = cols'
, dbRelations = rels
, dbPrimaryKeys = keys'
, dbProcs = procs
, pgVersion = pgVer
}
decodeTables :: HD.Result [Table]
decodeTables =
HD.rowList tblRow
where
tblRow = Table <$> HD.column HD.text
<*> HD.column HD.text
<*> HD.nullableColumn HD.text
<*> HD.column HD.bool
decodeColumns :: [Table] -> HD.Result [Column]
decodeColumns tables =
mapMaybe (columnFromRow tables) <$> HD.rowList colRow
where
colRow =
(,,,,,,,,,,,)
<$> HD.column HD.text <*> HD.column HD.text
<*> HD.column HD.text <*> HD.nullableColumn HD.text
<*> HD.column HD.int4 <*> HD.column HD.bool
<*> HD.column HD.text <*> HD.column HD.bool
<*> HD.nullableColumn HD.int4
<*> HD.nullableColumn HD.int4
<*> HD.nullableColumn HD.text
<*> HD.nullableColumn HD.text
decodeRelations :: [Table] -> [Column] -> HD.Result [Relation]
decodeRelations tables cols =
mapMaybe (relationFromRow tables cols) <$> HD.rowList relRow
where
relRow = (,,,,,)
<$> HD.column HD.text
<*> HD.column HD.text
<*> HD.column (HD.array (HD.dimension replicateM (HD.element HD.text)))
<*> HD.column HD.text
<*> HD.column HD.text
<*> HD.column (HD.array (HD.dimension replicateM (HD.element HD.text)))
decodePks :: [Table] -> HD.Result [PrimaryKey]
decodePks tables =
mapMaybe (pkFromRow tables) <$> HD.rowList pkRow
where
pkRow = (,,) <$> HD.column HD.text <*> HD.column HD.text <*> HD.column HD.text
decodeSynonyms :: [Column] -> HD.Result [Synonym]
decodeSynonyms cols =
mapMaybe (synonymFromRow cols) <$> HD.rowList synRow
where
synRow = (,,,,,)
<$> HD.column HD.text <*> HD.column HD.text
<*> HD.column HD.text <*> HD.column HD.text
<*> HD.column HD.text <*> HD.column HD.text
decodeProcs :: HD.Result (M.HashMap Text [ProcDescription])
decodeProcs =
-- Duplicate rows for a function means they're overloaded, order these by least args according to ProcDescription Ord instance
map sort . M.fromListWith (++) . map ((\(x,y) -> (x, [y])) . addName) <$> HD.rowList tblRow
where
tblRow = ProcDescription
<$> HD.column HD.text
<*> HD.nullableColumn HD.text
<*> (parseArgs <$> HD.column HD.text)
<*> (parseRetType
<$> HD.column HD.text
<*> HD.column HD.text
<*> HD.column HD.bool
<*> HD.column HD.char)
<*> (parseVolatility <$> HD.column HD.char)
addName :: ProcDescription -> (Text, ProcDescription)
addName pd = (pdName pd, pd)
parseArgs :: Text -> [PgArg]
parseArgs = mapMaybe parseArg . filter (not . isPrefixOf "OUT" . toS) . map strip . split (==',')
parseArg :: Text -> Maybe PgArg
parseArg a =
let arg = lastDef "" $ splitOn "INOUT " a
(body, def) = breakOn " DEFAULT " arg
(name, typ) = breakOn " " body in
if T.null typ
then Nothing
else Just $
PgArg (dropAround (== '"') name) (strip typ) (T.null def)
parseRetType :: Text -> Text -> Bool -> Char -> RetType
parseRetType schema name isSetOf typ
| isSetOf = SetOf pgType
| otherwise = Single pgType
where
qi = QualifiedIdentifier schema name
pgType = case typ of
'c' -> Composite qi
'p' -> if name == "record" -- Only pg pseudo type that is a row type is 'record'
then Composite qi
else Scalar qi
_ -> Scalar qi -- 'b'ase, 'd'omain, 'e'num, 'r'ange
parseVolatility :: Char -> ProcVolatility
parseVolatility v | v == 'i' = Immutable
| v == 's' = Stable
| otherwise = Volatile -- only 'v' can happen here
allProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
allProcs = H.Statement (toS procsSqlQuery) (HE.param HE.text) decodeProcs True
accessibleProcs :: H.Statement Schema (M.HashMap Text [ProcDescription])
accessibleProcs = H.Statement (toS sql) (HE.param HE.text) decodeProcs True
where
sql = procsSqlQuery <> " AND has_function_privilege(p.oid, 'execute')"
procsSqlQuery :: SqlQuery
procsSqlQuery = [q|
SELECT p.proname as "proc_name",
d.description as "proc_description",
pg_get_function_arguments(p.oid) as "args",
tn.nspname as "rettype_schema",
coalesce(comp.relname, t.typname) as "rettype_name",
p.proretset as "rettype_is_setof",
t.typtype as "rettype_typ",
p.provolatile
FROM pg_proc p
JOIN pg_namespace pn ON pn.oid = p.pronamespace
JOIN pg_type t ON t.oid = p.prorettype
JOIN pg_namespace tn ON tn.oid = t.typnamespace
LEFT JOIN pg_class comp ON comp.oid = t.typrelid
LEFT JOIN pg_catalog.pg_description as d on d.objoid = p.oid
WHERE pn.nspname = $1
|]
schemaDescription :: H.Statement Schema (Maybe Text)
schemaDescription =
H.Statement sql (HE.param HE.text) (join <$> HD.rowMaybe (HD.nullableColumn HD.text)) True
where
sql = [q|
select
description
from
pg_catalog.pg_namespace n
left join pg_catalog.pg_description d on d.objoid = n.oid
where
n.nspname = $1 |]
accessibleTables :: H.Statement Schema [Table]
accessibleTables =
H.Statement sql (HE.param HE.text) decodeTables True
where
sql = [q|
select
n.nspname as table_schema,
relname as table_name,
d.description as table_description,
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
left join pg_catalog.pg_description as d on d.objoid = c.oid and d.objsubid = 0
where
c.relkind in ('v', 'r', 'm', 'f')
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 |]
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
relToFk col Relation{relColumns=cols, relFColumns=colsF} = do
pos <- L.elemIndex col cols
colF <- atMay colsF pos
return $ ForeignKey colF
{-
Adds Views Child Relations based on Synonyms found, the logic is as follows:
Having a Relation{relTable=t1, relColumns=[c1], relFTable=t2, relFColumns=[c2], relType=Child} represented by:
t1.c1------t2.c2
When only having a t1_view.c1 synonym, we need to add a View to Table Child Relation
t1.c1----t2.c2 t1.c1----------t2.c2
-> ________/
/
t1_view.c1 t1_view.c1
When only having a t2_view.c2 synonym, we need to add a Table to View Child Relation
t1.c1----t2.c2 t1.c1----------t2.c2
-> \________
\
t2_view.c2 t2_view.c1
When having t1_view.c1 and a t2_view.c2 synonyms, we need to add a View to View Child Relation in addition to the prior
t1.c1----t2.c2 t1.c1----------t2.c2
-> \________/
/ \
t1_view.c1 t2_view.c2 t1_view.c1-------t2_view.c1
The logic for composite pks is similar just need to make sure all the Relation columns have synonyms.
-}
addViewChildRelations :: [Synonym] -> [Relation] -> [Relation]
addViewChildRelations allSyns = concatMap (\rel ->
rel : case rel of
Relation{relType=Child, relTable, relColumns, relFTable, relFColumns} ->
let colSynsGroupedByView :: [Column] -> [[Synonym]]
colSynsGroupedByView relCols = L.groupBy (\(_, viewCol1) (_, viewCol2) -> colTable viewCol1 == colTable viewCol2) $
filter (\(c, _) -> c `elem` relCols) allSyns
colsSyns = colSynsGroupedByView relColumns
fColsSyns = colSynsGroupedByView relFColumns
getView :: [Synonym] -> Table
getView = colTable . snd . unsafeHead
syns `allSynsOf` cols = S.fromList (fst <$> syns) == S.fromList cols
-- Relation is dependent on the order of relColumns and relFColumns to get the join conditions right in the generated query.
-- So we need to change the order of the synonyms to match the relColumns
-- This could be avoided if the Relation type is improved with a structure that maintains the association of relColumns and relFColumns
syns `sortAccordingTo` columns = sortOn (\(k, _) -> L.lookup k $ zip columns [0::Int ..]) syns in
-- View Table Child Relations
[Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) relFTable relFColumns Child Nothing Nothing Nothing
| syns <- colsSyns, syns `allSynsOf` relColumns] ++
-- Table View Child Relations
[Relation relTable relColumns (getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) Child Nothing Nothing Nothing
| fSyns <- fColsSyns, fSyns `allSynsOf` relFColumns] ++
-- View View Child Relations
[Relation (getView syns) (snd <$> syns `sortAccordingTo` relColumns) (getView fSyns) (snd <$> fSyns `sortAccordingTo` relFColumns) Child Nothing Nothing Nothing
| syns <- colsSyns, fSyns <- fColsSyns, syns `allSynsOf` relColumns, fSyns `allSynsOf` relFColumns]
_ -> [])
addParentRelations :: [Relation] -> [Relation]
addParentRelations = concatMap (\rel@(Relation t c ft fc _ _ _ _) -> [rel, Relation ft fc t c Parent Nothing Nothing Nothing])
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
-- Reference : https://wiki.haskell.org/99_questions/Solutions/26
combinations :: Int -> [a] -> [[a]]
combinations 0 _ = [ [] ]
combinations n xs = [ y:ys | y:xs' <- tails xs
, ys <- combinations (n-1) xs']
addMirrorRelation = concatMap (\rel@(Relation t c ft fc _ lt lc1 lc2) -> [rel, Relation ft fc t c Many lt lc2 lc1])
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
addViewPrimaryKeys :: [Synonym] -> [PrimaryKey] -> [PrimaryKey]
addViewPrimaryKeys syns = concatMap (\pk ->
let viewPks = (\(_, viewCol) -> PrimaryKey{pkTable=colTable viewCol, pkName=colName viewCol}) <$>
filter (\(col, _) -> colTable col == pkTable pk && colName col == pkName pk) syns in
pk : viewPks)
allTables :: H.Statement () [Table]
allTables =
H.Statement sql HE.unit decodeTables True
where
sql = [q|
SELECT
n.nspname AS table_schema,
c.relname AS table_name,
NULL AS table_description,
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','f')
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.Statement Schema [Column]
allColumns tabs =
H.Statement sql (HE.param HE.text) (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.description AS description,
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 pg_catalog to get only Primary and Foreign key columns outside api schema
*/
WITH key_columns AS (
SELECT
r.oid AS r_oid,
c.oid AS c_oid,
n.nspname,
c.relname,
r.conname,
r.contype,
unnest(r.conkey) AS conkey
FROM
pg_catalog.pg_constraint r,
pg_catalog.pg_class c,
pg_catalog.pg_namespace n
WHERE
r.contype IN ('f', 'p')
AND c.relkind IN ('r', 'v', 'f', 'm')
AND r.conrelid = c.oid
AND c.relnamespace = n.oid
AND n.nspname NOT IN ('pg_catalog', 'information_schema', $1)
),
/*
-- CTE based on information_schema.columns
-- changed:
-- remove the owner filter
-- limit columns to the ones in the api schema or PK/FK columns
*/
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,
d.description::information_schema.sql_identifier AS description,
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 format_type(a.atttypid, a.atttypmod)
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 format_type(a.atttypid, a.atttypmod)
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 key_columns kc ON kc.conkey = a.attnum AND kc.c_oid = a.attrelid
LEFT JOIN pg_catalog.pg_description AS d ON d.objoid = a.attrelid and d.objsubid = a.attnum
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", 'm'::"char"]))
AND (nc.nspname = $1 OR kc.r_oid IS NOT NULL) /*--filter only columns that are FK/PK or in the api schema */
/*--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,
description,
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,
Maybe Text, Int32, Bool,
Text, Bool, Maybe Int32,
Maybe Int32, Maybe Text, Maybe Text)
-> Maybe Column
columnFromRow tabs (s, t, n, desc, pos, nul, typ, u, l, p, d, e) = buildColumn <$> table
where
buildColumn tbl = Column tbl n desc 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 = maybe [] (split (==','))
allChildRelations :: [Table] -> [Column] -> H.Statement () [Relation]
allChildRelations 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.Statement () [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] -> PgVersion -> H.Statement Schema [Synonym]
allSynonyms cols pgVer =
H.Statement sql (HE.param HE.text) (decodeSynonyms cols) True
-- query explanation at https://gist.github.com/steve-chavez/7ee0e6590cddafb532e5f00c46275569
where
subselectRegex :: Text
subselectRegex | pgVer < pgVersion100 = ":subselect {.*?:constraintDeps <>} :location"
| otherwise = ":subselect {.*?:stmt_len 0} :location"
sql = [qc|
with
views as (
select
n.nspname as view_schema,
c.relname as view_name,
r.ev_action as view_definition
from pg_class c
join pg_namespace n on n.oid = c.relnamespace
join pg_rewrite r on r.ev_class = c.oid
where (c.relkind = 'v'::char) and n.nspname = $1
),
removed_subselects as(
select
view_schema, view_name,
regexp_replace(view_definition, '{subselectRegex}', '', 'g') as x
from views
),
target_lists as(
select
view_schema, view_name,
regexp_split_to_array(x, 'targetList') as x
from removed_subselects
),
last_target_list_wo_tail as(
select
view_schema, view_name,
(regexp_split_to_array(x[array_upper(x, 1)], ':onConflict'))[1] as x
from target_lists
),
target_entries as(
select
view_schema, view_name,
unnest(regexp_split_to_array(x, 'TARGETENTRY')) as entry
from last_target_list_wo_tail
),
results as(
select
view_schema, view_name,
substring(entry from ':resname (.*?) :') as view_colum_name,
substring(entry from ':resorigtbl (.*?) :') as resorigtbl,
substring(entry from ':resorigcol (.*?) :') as resorigcol
from target_entries
)
select
sch.nspname as table_schema,
tbl.relname as table_name,
col.attname as table_column_name,
res.view_schema,
res.view_name,
res.view_colum_name
from results res
join pg_class tbl on tbl.oid::text = res.resorigtbl
join pg_attribute col on col.attrelid = tbl.oid and col.attnum::text = res.resorigcol
join pg_namespace sch on sch.oid = tbl.relnamespace
where resorigtbl <> '0'
order by view_schema, view_name, view_colum_name; |]
synonymFromRow :: [Column] -> (Text,Text,Text,Text,Text,Text) -> Maybe Synonym
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
getPgVersion :: H.Session PgVersion
getPgVersion = H.statement () $ H.Statement sql HE.unit versionRow False
where
sql = "SELECT current_setting('server_version_num')::integer, current_setting('server_version')"
versionRow = HD.singleRow $ PgVersion <$> HD.column HD.int4 <*> HD.column HD.text
+163 -43
View File
@@ -1,53 +1,170 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE FlexibleInstances, TypeSynonymInstances #-}
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE TypeSynonymInstances #-}
module PostgREST.Error (PgError, errResponse) where
module PostgREST.Error (
apiRequestError
, pgError
, simpleError
, singularityError
, binaryFieldError
, connectionLostError
, encodeError
, gucHeadersError
) where
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Protolude
import Data.Aeson ((.=))
import qualified Data.Aeson as JSON
import Data.Text (unwords)
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 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
import Network.Wai (Response, responseLBS)
import PostgREST.Types
import Text.Read (readMaybe)
type PgError = H.SessionError P.Postgres
apiRequestError :: ApiRequestError -> Response
apiRequestError err =
errorResponse status
[toHeader CTApplicationJSON] err
where
status =
case err of
ActionInappropriate -> HT.status405
UnsupportedVerb -> HT.status405
InvalidBody _ -> HT.status400
ParseRequestError _ _ -> HT.status400
NoRelationBetween _ _ -> HT.status400
InvalidRange -> HT.status416
UnknownRelation -> HT.status404
InvalidFilters -> HT.status405
errResponse :: PgError -> Response
errResponse e = responseLBS (httpStatus e)
[(hContentType, "application/json")] (JSON.encode e)
simpleError :: HT.Status -> [Header] -> Text -> Response
simpleError status hdrs message =
errorResponse status (toHeader CTApplicationJSON : hdrs) $
JSON.object ["message" .= message]
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]
errorResponse :: JSON.ToJSON a => HT.Status -> [Header] -> a -> Response
errorResponse status hdrs e =
responseLBS status hdrs $ encodeError e
httpStatus :: PgError -> HT.Status
httpStatus (H.TxError (P.ErroneousResult codeBS _ _ _)) =
let code = cs codeBS in
case code of
pgError :: Bool -> P.UsageError -> Response
pgError authed e =
let status = httpStatus authed e
jsonType = toHeader CTApplicationJSON
wwwAuth = ("WWW-Authenticate", "Bearer")
hdrs = if status == HT.status401
then [jsonType, wwwAuth]
else [jsonType] in
responseLBS status hdrs (encodeError e)
singularityError :: Integer -> Response
singularityError numRows =
responseLBS HT.status406
[toHeader CTSingularJSON]
$ toS . formatGeneralError
"JSON object requested, multiple (or no) rows returned"
$ unwords
[ "Results contain", show numRows, "rows,"
, toS (toMime CTSingularJSON), "requires 1 row"
]
where
formatGeneralError :: Text -> Text -> Text
formatGeneralError message details = toS . JSON.encode $
JSON.object ["message" .= message, "details" .= details]
binaryFieldError :: Response
binaryFieldError =
simpleError HT.status406 [] (toS (toMime CTOctetStream) <>
" requested but a single column was not selected")
gucHeadersError :: Response
gucHeadersError =
simpleError HT.status500 []
"response.headers guc must be a JSON array composed of objects with a single key and a string value"
connectionLostError :: Response
connectionLostError =
simpleError HT.status503 [] "Database connection lost, retrying the connection."
encodeError :: JSON.ToJSON a => a -> LByteString
encodeError = JSON.encode
instance JSON.ToJSON ApiRequestError where
toJSON (ParseRequestError message details) = JSON.object [
"message" .= message, "details" .= details]
toJSON ActionInappropriate = JSON.object [
"message" .= ("Bad Request" :: Text)]
toJSON (InvalidBody errorMessage) = JSON.object [
"message" .= (toS errorMessage :: Text)]
toJSON InvalidRange = JSON.object [
"message" .= ("HTTP Range error" :: Text)]
toJSON UnknownRelation = JSON.object [
"message" .= ("Unknown relation" :: Text)]
toJSON (NoRelationBetween parent child) = JSON.object [
"message" .= ("Could not find foreign keys between these entities, No relation found between " <> parent <> " and " <> child :: Text)]
toJSON UnsupportedVerb = JSON.object [
"message" .= ("Unsupported HTTP verb" :: Text)]
toJSON InvalidFilters = JSON.object [
"message" .= ("Filters must include all and only primary key columns with 'eq' operators" :: Text)]
instance JSON.ToJSON P.UsageError where
toJSON (P.ConnectionError e) = JSON.object [
"code" .= ("" :: Text),
"message" .= ("Database connection error" :: Text),
"details" .= (toS $ fromMaybe "" e :: Text)]
toJSON (P.SessionError e) = JSON.toJSON e -- H.Error
instance JSON.ToJSON H.QueryError where
toJSON (H.QueryError _ _ e) = JSON.toJSON e
instance JSON.ToJSON H.CommandError where
toJSON (H.ResultError (H.ServerError c m d h)) = case toS c of
'P':'T':_ ->
JSON.object [
"details" .= (fmap toS d::Maybe Text),
"hint" .= (fmap toS h::Maybe Text)]
_ ->
JSON.object [
"code" .= (toS c::Text),
"message" .= (toS m::Text),
"details" .= (fmap toS d::Maybe Text),
"hint" .= (fmap toS h::Maybe Text)]
toJSON (H.ResultError (H.UnexpectedResult m)) = JSON.object [
"message" .= (m::Text)]
toJSON (H.ResultError (H.RowError i H.EndOfInput)) = JSON.object [
"message" .= ("Row error: end of input"::Text),
"details" .=
("Attempt to parse more columns than there are in the result"::Text),
"details" .= (("Row number " <> show i)::Text)]
toJSON (H.ResultError (H.RowError i H.UnexpectedNull)) = JSON.object [
"message" .= ("Row error: unexpected null"::Text),
"details" .= ("Attempt to parse a NULL as some value."::Text),
"details" .= (("Row number " <> show i)::Text)]
toJSON (H.ResultError (H.RowError i (H.ValueError d))) = JSON.object [
"message" .= ("Row error: Wrong value parser used"::Text),
"details" .= d,
"details" .= (("Row number " <> show i)::Text)]
toJSON (H.ResultError (H.UnexpectedAmountOfRows i)) = JSON.object [
"message" .= ("Unexpected amount of rows"::Text),
"details" .= i]
toJSON (H.ClientError d) = JSON.object [
"message" .= ("Database client error"::Text),
"details" .= (fmap toS d::Maybe Text)]
httpStatus :: Bool -> P.UsageError -> HT.Status
httpStatus _ (P.ConnectionError _) = HT.status503
httpStatus authed (P.SessionError (H.QueryError _ _ (H.ResultError (H.ServerError c m _ _)))) =
case toS 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
@@ -62,10 +179,13 @@ httpStatus (H.TxError (P.ErroneousResult codeBS _ _ _)) =
'5':'8':_ -> HT.status500 -- system error
'F':'0':_ -> HT.status500 -- conf file error
'H':'V':_ -> HT.status500 -- foreign data wrapper error
"P0001" -> HT.status400 -- default code for "raise"
'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
"42883" -> HT.status404 -- undefined function
"42P01" -> HT.status404 -- undefined table
"42501" -> if authed then HT.status403 else HT.status401 -- insufficient privilege
'P':'T':n -> fromMaybe HT.status500 (HT.mkStatus <$> readMaybe n <*> pure m)
_ -> HT.status400
httpStatus _ (P.SessionError (H.QueryError _ _ (H.ResultError _))) = HT.status500
httpStatus _ (P.SessionError (H.QueryError _ _ (H.ClientError _))) = HT.status503
-81
View File
@@ -1,81 +0,0 @@
{-# LANGUAGE QuasiQuotes, ScopedTypeVariables #-}
module Main where
import Paths_postgrest (version)
import PostgREST.App
import PostgREST.Middleware
import PostgREST.Error(errResponse)
import Control.Monad (unless)
import Control.Monad.IO.Class (liftIO)
import Data.String.Conversions (cs)
import Network.Wai (strictRequestBody)
import Network.Wai.Handler.Warp hiding (Connection)
import Network.Wai.Middleware.RequestLogger (logStdout)
import Data.List (intercalate)
import Data.Version (versionBranch)
import Data.Functor.Identity
import Data.Text(Text)
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Options.Applicative hiding (columns)
import System.IO (stderr, stdin, stdout, hSetBuffering, BufferMode(..))
import PostgREST.Config (AppConfig(..), argParser)
isServerVersionSupported = do
Identity (row :: Text) <- H.tx Nothing $ H.singleEx $ [H.stmt|SHOW server_version_num|]
return $ read (cs row) >= 90200
main :: IO ()
main = do
hSetBuffering stdout LineBuffering
hSetBuffering stdin LineBuffering
hSetBuffering stderr NoBuffering
let opts = info (helper <*> argParser) $
fullDesc
<> progDesc (
"PostgREST "
<> prettyVersion
<> " / create a REST API to an existing Postgres database"
)
parserPrefs = prefs showHelpOnError
conf <- customExecParser parserPrefs opts
let port = configPort conf
unless (configSecure conf) $
putStrLn "WARNING, running in insecure mode, auth will be in plaintext"
unless ("secret" /= configJwtSecret conf) $
putStrLn "WARNING, running in insecure mode, JWT secret is the default value"
Prelude.putStrLn $ "Listening on port " ++
(show $ configPort conf :: String)
let pgSettings = P.ParamSettings (cs $ configDbHost conf)
(fromIntegral $ configDbPort conf)
(cs $ configDbUser conf)
(cs $ configDbPass conf)
(cs $ configDbName conf)
appSettings = setPort port
. setServerName (cs $ "postgrest/" <> prettyVersion)
$ defaultSettings
middle = logStdout . defaultMiddle (configSecure conf)
poolSettings <- maybe (fail "Improper session settings") return $
H.poolSettings (fromIntegral $ configPool conf) 30
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings poolSettings
resOrError <- H.session pool isServerVersionSupported
either (fail . show) (\supported -> unless supported $ fail "Cannot run in this PostgreSQL version, PostgREST needs at least 9.2.0") resOrError
runSettings appSettings $ middle $ \req respond -> do
body <- strictRequestBody req
resOrError <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True)) $
authenticated conf (app conf body) req
either (respond . errResponse) respond resOrError
where
prettyVersion = intercalate "." $ map show $ versionBranch version
+53 -91
View File
@@ -1,101 +1,63 @@
{-# OPTIONS_GHC -fno-warn-orphans #-}
{-# LANGUAGE ScopedTypeVariables #-}
{-# LANGUAGE FlexibleContexts #-}
module PostgREST.Middleware where
import Data.Maybe (fromMaybe, isNothing)
import Data.Monoid
import Data.Text
-- import Data.Pool(withResource, Pool)
import Crypto.JWT
import qualified Data.Aeson as JSON
import qualified Data.HashMap.Strict as M
import qualified Hasql.Transaction as H
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Data.String.Conversions(cs)
import Network.HTTP.Types.Status (unauthorized401, status500)
import Network.Wai (Application, Response)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Gzip (def, gzip)
import Network.Wai.Middleware.Static (only, staticPolicy)
import Network.HTTP.Types.Header (hLocation, hAuthorization, hAccept)
import Network.HTTP.Types (RequestHeaders)
import Network.HTTP.Types.Status (status400, status401, status301, status415)
import Network.Wai (Application, requestHeaders, responseLBS, rawPathInfo,
rawQueryString, isSecure, Request(..), Response)
import Network.Wai.Middleware.Gzip (gzip, def)
import Network.Wai.Middleware.Cors (cors)
import Network.Wai.Middleware.Static (staticPolicy, only)
import Network.URI (URI(..), parseURI)
import PostgREST.ApiRequest (ApiRequest(..))
import PostgREST.Auth (JWTAttempt(..))
import PostgREST.Config (AppConfig (..), corsPolicy)
import PostgREST.Error (simpleError)
import PostgREST.QueryBuilder (unquoted, pgFmtSetLocal, pgFmtSetLocalSearchPath)
import PostgREST.Config (AppConfig(..), corsPolicy)
import PostgREST.Auth (LoginAttempt(..), signInRole, signInWithJWT, setRole, setUserId)
import PostgREST.App (contentTypeForAccept)
import Codec.Binary.Base64.String (decode)
import Protolude
import Prelude
runWithClaims :: AppConfig -> JWTAttempt ->
(ApiRequest -> H.Transaction Response) ->
ApiRequest -> H.Transaction Response
runWithClaims conf eClaims app req =
case eClaims of
JWTInvalid JWTExpired -> return $ unauthed "JWT expired"
JWTInvalid e -> return $ unauthed $ show e
JWTMissingSecret -> return $ simpleError status500 [] "Server lacks JWT secret"
JWTClaims claims -> do
H.sql $ toS . mconcat $ setSearchPathSql : setRoleSql ++ claimsSql ++ headersSql ++ cookiesSql ++ appSettingsSql
mapM_ H.sql customReqCheck
app req
where
headersSql = pgFmtSetLocal "request.header." <$> iHeaders req
cookiesSql = pgFmtSetLocal "request.cookie." <$> iCookies req
claimsSql = pgFmtSetLocal "request.jwt.claim." <$> [(c,unquoted v) | (c,v) <- M.toList claimsWithRole]
appSettingsSql = pgFmtSetLocal mempty <$> configSettings conf
setRoleSql = maybeToList $ (\x ->
pgFmtSetLocal mempty ("role", unquoted x)) <$> M.lookup "role" claimsWithRole
setSearchPathSql = pgFmtSetLocalSearchPath $ configSchema conf : configExtraSearchPath conf
-- role claim defaults to anon if not specified in jwt
claimsWithRole = M.union claims (M.singleton "role" anon)
anon = JSON.String . toS $ configAnonRole conf
customReqCheck = (\f -> "select " <> toS f <> "();") <$> configReqCheck conf
where
unauthed message = simpleError
unauthorized401
[( "WWW-Authenticate"
, "Bearer error=\"invalid_token\", " <>
"error_description=" <> show message
)]
message
authenticated :: forall s. AppConfig ->
(Request -> H.Tx P.Postgres s Response) ->
Request -> H.Tx P.Postgres s Response
authenticated conf 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 uid -> if role /= currentRole then runInRole role uid else app req
NoCredentials -> if anon /= currentRole then runInRole anon "" else app req
where
jwtSecret = cs $ configJwtSecret conf
currentRole = cs $ configDbUser conf
anon = cs $ configAnonRole conf
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
("Bearer" : jwt : _) ->
return $ signInWithJWT jwtSecret jwt
_ -> return NoCredentials
runInRole :: Text -> Text -> H.Tx P.Postgres s Response
runInRole r uid = do
setUserId uid
setRole r
app req
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
unsupportedAccept :: Application -> Application
unsupportedAccept app req respond = do
let
accept = lookup hAccept $ requestHeaders req
if isNothing $ contentTypeForAccept accept
then respond $ responseLBS status415 [] "Unsupported Accept header, try: application/json"
else app req respond
defaultMiddle :: Bool -> Application -> Application
defaultMiddle secure = (if secure then redirectInsecure else id)
. gzip def . cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
. unsupportedAccept
defaultMiddle :: Application -> Application
defaultMiddle =
gzip def
. cors corsPolicy
. staticPolicy (only [("favicon.ico", "static/favicon.ico")])
+369
View File
@@ -0,0 +1,369 @@
{-# LANGUAGE OverloadedStrings #-}
module PostgREST.OpenAPI (
encodeOpenAPI
, isMalformedProxyUri
, pickProxy
) where
import Control.Arrow ((&&&))
import Control.Lens
import Data.Aeson (decode, encode)
import Data.HashMap.Strict.InsOrd (InsOrdHashMap, fromList)
import Data.Maybe (fromJust)
import qualified Data.Set as Set
import Data.String (IsString (..))
import Data.Text (unpack, pack, init, tail, toLower, intercalate, append, dropWhile, breakOn)
import Network.URI (parseURI, isAbsoluteURI,
URI (..), URIAuth (..))
import Protolude hiding ((&), Proxy, get, intercalate, dropWhile)
import Data.Swagger
import PostgREST.ApiRequest (ContentType(..))
import PostgREST.Config (prettyVersion, docsVersion)
import PostgREST.Types (Table(..), Column(..), PgArg(..), ForeignKey(..),
PrimaryKey(..), Proxy(..), ProcDescription(..), toMime)
makeMimeList :: [ContentType] -> MimeList
makeMimeList cs = MimeList $ map (fromString . toS . toMime) cs
toSwaggerType :: Text -> SwaggerType t
toSwaggerType "text" = SwaggerString
toSwaggerType "integer" = SwaggerInteger
toSwaggerType "boolean" = SwaggerBoolean
toSwaggerType "numeric" = SwaggerNumber
toSwaggerType _ = SwaggerString
makeTableDef :: [PrimaryKey] -> (Table, [Column], [Text]) -> (Text, Schema)
makeTableDef pks (t, cs, _) =
let tn = tableName t in
(tn, (mempty :: Schema)
& description .~ tableDescription t
& type_ .~ SwaggerObject
& properties .~ fromList (map (makeProperty pks) cs)
& required .~ map colName (filter (not . colNullable) cs))
makeProperty :: [PrimaryKey] -> Column -> (Text, Referenced Schema)
makeProperty pks c = (colName c, Inline s)
where
e = if null $ colEnum c then Nothing else decode $ encode $ colEnum c
fk ForeignKey{fkCol=Column{colTable=Table{tableName=a}, colName=b}} =
intercalate "" ["This is a Foreign Key to `", a, ".", b, "`.<fk table='", a, "' column='", b, "'/>"]
pk :: Bool
pk = any (\p -> pkTable p == colTable c && pkName p == colName c) pks
n = catMaybes
[ Just "Note:"
, if pk then Just "This is a Primary Key.<pk/>" else Nothing
, fk <$> colFK c
]
d =
if length n > 1 then
Just $ append (maybe "" (`append` "\n\n") $ colDescription c) (intercalate "\n" n)
else
colDescription c
s =
(mempty :: Schema)
& default_ .~ (decode . toS =<< colDefault c)
& description .~ d
& enum_ .~ e
& format ?~ colType c
& maxLength .~ (fromIntegral <$> colMaxLen c)
& type_ .~ toSwaggerType (colType c)
makeProcSchema :: ProcDescription -> Schema
makeProcSchema pd =
(mempty :: Schema)
& description .~ pdDescription pd
& type_ .~ SwaggerObject
& properties .~ fromList (map makeProcProperty (pdArgs pd))
& required .~ map pgaName (filter pgaReq (pdArgs pd))
makeProcProperty :: PgArg -> (Text, Referenced Schema)
makeProcProperty (PgArg n t _) = (n, Inline s)
where
s = (mempty :: Schema)
& type_ .~ toSwaggerType t
& format ?~ t
makePreferParam :: [Text] -> Param
makePreferParam ts =
(mempty :: Param)
& name .~ "Prefer"
& description ?~ "Preference"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader
& type_ .~ SwaggerString
& enum_ .~ decode (encode ts))
makeProcParam :: ProcDescription -> [Referenced Param]
makeProcParam pd =
[ Inline $ (mempty :: Param)
& name .~ "args"
& required ?~ True
& schema .~ (ParamBody $ Inline $ makeProcSchema pd)
, Ref $ Reference "preferParams"
]
makeParamDefs :: [(Table, [Column], [Text])] -> [(Text, Param)]
makeParamDefs ti =
[ ("preferParams", makePreferParam ["params=single-object"])
, ("preferReturn", makePreferParam ["return=representation", "return=minimal", "return=none"])
, ("preferCount", makePreferParam ["count=none"])
, ("select", (mempty :: Param)
& name .~ "select"
& description ?~ "Filtering Columns"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery
& type_ .~ SwaggerString))
, ("order", (mempty :: Param)
& name .~ "order"
& description ?~ "Ordering"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery
& type_ .~ SwaggerString))
, ("range", (mempty :: Param)
& name .~ "Range"
& description ?~ "Limiting and Pagination"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader
& type_ .~ SwaggerString))
, ("rangeUnit", (mempty :: Param)
& name .~ "Range-Unit"
& description ?~ "Limiting and Pagination"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamHeader
& type_ .~ SwaggerString
& default_ .~ decode "\"items\""))
, ("offset", (mempty :: Param)
& name .~ "offset"
& description ?~ "Limiting and Pagination"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery
& type_ .~ SwaggerString))
, ("limit", (mempty :: Param)
& name .~ "limit"
& description ?~ "Limiting and Pagination"
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery
& type_ .~ SwaggerString))
]
<> concat [ makeObjectBody (tableName t) : makeRowFilters (tableName t) cs
| (t, cs, _) <- ti
]
makeObjectBody :: Text -> (Text, Param)
makeObjectBody tn =
("body." <> tn, (mempty :: Param)
& name .~ tn
& description ?~ tn
& required ?~ False
& schema .~ ParamBody (Ref (Reference tn)))
makeRowFilter :: Text -> Column -> (Text, Param)
makeRowFilter tn c =
(intercalate "." ["rowFilter", tn, colName c], (mempty :: Param)
& name .~ colName c
& description .~ colDescription c
& required ?~ False
& schema .~ ParamOther ((mempty :: ParamOtherSchema)
& in_ .~ ParamQuery
& type_ .~ SwaggerString
& format ?~ colType c))
makeRowFilters :: Text -> [Column] -> [(Text, Param)]
makeRowFilters tn = map (makeRowFilter tn)
makePathItem :: (Table, [Column], [Text]) -> (FilePath, PathItem)
makePathItem (t, cs, _) = ("/" ++ unpack tn, p $ tableInsertable t)
where
-- Use first line of table description as summary; rest as description (if present)
-- We strip leading newlines from description so that users can include a blank line between summary and description
(tSum, tDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
breakOn "\n" <$> tableDescription t
tOp = (mempty :: Operation)
& tags .~ Set.fromList [tn]
& summary .~ tSum
& description .~ mfilter (/="") tDesc
getOp = tOp
& parameters .~ map ref (rs <> ["select", "order", "range", "rangeUnit", "offset", "limit", "preferCount"])
& at 206 ?~ "Partial Content"
& at 200 ?~ Inline ((mempty :: Response)
& description .~ "OK"
& schema ?~ Inline (mempty
& type_ .~ SwaggerArray
& items ?~ (SwaggerItemsObject $ Ref $ Reference $ tableName t)
)
)
postOp = tOp
& parameters .~ map ref ["body." <> tn, "preferReturn"]
& at 201 ?~ "Created"
patchOp = tOp
& parameters .~ map ref (rs <> ["body." <> tn, "preferReturn"])
& at 204 ?~ "No Content"
deletOp = tOp
& parameters .~ map ref (rs <> ["preferReturn"])
& at 204 ?~ "No Content"
pr = (mempty :: PathItem) & get ?~ getOp
pw = pr & post ?~ postOp & patch ?~ patchOp & delete ?~ deletOp
p False = pr
p True = pw
tn = tableName t
rs = [ intercalate "." ["rowFilter", tn, colName c ] | c <- cs ]
ref = Ref . Reference
makeProcPathItem :: ProcDescription -> (FilePath, PathItem)
makeProcPathItem pd = ("/rpc/" ++ toS (pdName pd), pe)
where
-- Use first line of proc description as summary; rest as description (if present)
-- We strip leading newlines from description so that users can include a blank line between summary and description
(pSum, pDesc) = fmap fst &&& fmap (dropWhile (=='\n') . snd) $
breakOn "\n" <$> pdDescription pd
postOp = (mempty :: Operation)
& summary .~ pSum
& description .~ mfilter (/="") pDesc
& parameters .~ makeProcParam pd
& tags .~ Set.fromList ["(rpc) " <> pdName pd]
& produces ?~ makeMimeList [CTApplicationJSON, CTSingularJSON]
& at 200 ?~ "OK"
pe = (mempty :: PathItem) & post ?~ postOp
makeRootPathItem :: (FilePath, PathItem)
makeRootPathItem = ("/", p)
where
getOp = (mempty :: Operation)
& tags .~ Set.fromList ["Introspection"]
& summary ?~ "OpenAPI description (this document)"
& produces ?~ makeMimeList [CTOpenAPI, CTApplicationJSON]
& at 200 ?~ "OK"
pr = (mempty :: PathItem) & get ?~ getOp
p = pr
makePathItems :: [ProcDescription] -> [(Table, [Column], [Text])] -> InsOrdHashMap FilePath PathItem
makePathItems pds ti = fromList $ makeRootPathItem :
map makePathItem ti ++ map makeProcPathItem pds
escapeHostName :: Text -> Text
escapeHostName "*" = "0.0.0.0"
escapeHostName "*4" = "0.0.0.0"
escapeHostName "!4" = "0.0.0.0"
escapeHostName "*6" = "0.0.0.0"
escapeHostName "!6" = "0.0.0.0"
escapeHostName h = h
postgrestSpec :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> Swagger
postgrestSpec pds ti (s, h, p, b) sd pks = (mempty :: Swagger)
& basePath ?~ unpack b
& schemes ?~ [s']
& info .~ ((mempty :: Info)
& version .~ prettyVersion
& title .~ "PostgREST API"
& description ?~ d)
& externalDocs ?~ ((mempty :: ExternalDocs)
& description ?~ "PostgREST Documentation"
& url .~ URL ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
& host .~ h'
& definitions .~ fromList (map (makeTableDef pks) ti)
& parameters .~ fromList (makeParamDefs ti)
& paths .~ makePathItems pds ti
& produces .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
& consumes .~ makeMimeList [CTApplicationJSON, CTSingularJSON, CTTextCSV]
where
s' = if s == "http" then Http else Https
h' = Just $ Host (unpack $ escapeHostName h) (Just (fromInteger p))
d = fromMaybe "This is a dynamic API generated by PostgREST" sd
encodeOpenAPI :: [ProcDescription] -> [(Table, [Column], [Text])] -> (Text, Text, Integer, Text) -> Maybe Text -> [PrimaryKey] -> LByteString
encodeOpenAPI pds ti uri sd pks = encode $ postgrestSpec pds ti uri sd pks
{-|
Test whether a proxy uri is malformed or not.
A valid proxy uri should be an absolute uri without query and user info,
only http(s) schemes are valid, port number range is 1-65535.
For example
http://postgrest.com/openapi.json
https://postgrest.com:8080/openapi.json
-}
isMalformedProxyUri :: Maybe Text -> Bool
isMalformedProxyUri Nothing = False
isMalformedProxyUri (Just uri)
| isAbsoluteURI (toS uri) = not $ isUriValid $ toURI uri
| otherwise = True
toURI :: Text -> URI
toURI uri = fromJust $ parseURI (toS uri)
pickProxy :: Maybe Text -> Maybe Proxy
pickProxy proxy
| isNothing proxy = Nothing
-- should never happen
-- since the request would have been rejected by the middleware if proxy uri
-- is malformed
| isMalformedProxyUri proxy = Nothing
| otherwise = Just Proxy {
proxyScheme = scheme
, proxyHost = host'
, proxyPort = port''
, proxyPath = path'
}
where
uri = toURI $ fromJust proxy
scheme = init $ toLower $ pack $ uriScheme uri
path URI {uriPath = ""} = "/"
path URI {uriPath = p} = p
path' = pack $ path uri
authority = fromJust $ uriAuthority uri
host' = pack $ uriRegName authority
port' = uriPort authority
readPort = fromMaybe 80 . readMaybe
port'' :: Integer
port'' = case (port', scheme) of
("", "http") -> 80
("", "https") -> 443
_ -> readPort $ unpack $ tail $ pack port'
isUriValid:: URI -> Bool
isUriValid = fAnd [isSchemeValid, isQueryValid, isAuthorityValid]
fAnd :: [a -> Bool] -> a -> Bool
fAnd fs x = all ($x) fs
isSchemeValid :: URI -> Bool
isSchemeValid URI {uriScheme = s}
| toLower (pack s) == "https:" = True
| toLower (pack s) == "http:" = True
| otherwise = False
isQueryValid :: URI -> Bool
isQueryValid URI {uriQuery = ""} = True
isQueryValid _ = False
isAuthorityValid :: URI -> Bool
isAuthorityValid URI {uriAuthority = a}
| isJust a = fAnd [isUserInfoValid, isHostValid, isPortValid] $ fromJust a
| otherwise = False
isUserInfoValid :: URIAuth -> Bool
isUserInfoValid URIAuth {uriUserInfo = ""} = True
isUserInfoValid _ = False
isHostValid :: URIAuth -> Bool
isHostValid URIAuth {uriRegName = ""} = False
isHostValid _ = True
isPortValid :: URIAuth -> Bool
isPortValid URIAuth {uriPort = ""} = True
isPortValid URIAuth {uriPort = (':':p)} =
case readMaybe p of
Just i -> i > (0 :: Integer) && i < 65536
Nothing -> False
isPortValid _ = False
+242
View File
@@ -0,0 +1,242 @@
module PostgREST.Parsers where
import Protolude hiding (try, intercalate, replace, option)
import Control.Monad ((>>))
import Data.Foldable (foldl1)
import Data.Functor (($>))
import qualified Data.HashMap.Strict as M
import Data.Text (intercalate, replace, strip)
import Data.List (init, last)
import Data.Tree
import Data.Either.Combinators (mapLeft)
import PostgREST.RangeQuery (NonnegRange)
import PostgREST.Types
import Text.ParserCombinators.Parsec hiding (many, (<|>))
import Text.Parsec.Error
import Text.Read (read)
pRequestSelect :: Text -> Either ApiRequestError [Tree SelectItem]
pRequestSelect selStr =
mapError $ parse pFieldForest ("failed to parse select parameter (" <> toS selStr <> ")") (toS selStr)
pRequestFilter :: (Text, Text) -> Either ApiRequestError (EmbedPath, Filter)
pRequestFilter (k, v) = mapError $ (,) <$> path <*> (Filter <$> fld <*> oper)
where
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
oper = parse (pOpExpr pSingleVal) ("failed to parse filter (" ++ toS v ++ ")") $ toS v
path = fst <$> treePath
fld = snd <$> treePath
pRequestOrder :: (Text, Text) -> Either ApiRequestError (EmbedPath, [OrderTerm])
pRequestOrder (k, v) = mapError $ (,) <$> path <*> ord'
where
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
path = fst <$> treePath
ord' = parse pOrder ("failed to parse order (" ++ toS v ++ ")") $ toS v
pRequestRange :: (ByteString, NonnegRange) -> Either ApiRequestError (EmbedPath, NonnegRange)
pRequestRange (k, v) = mapError $ (,) <$> path <*> pure v
where
treePath = parse pTreePath ("failed to parser tree path (" ++ toS k ++ ")") $ toS k
path = fst <$> treePath
pRequestLogicTree :: (Text, Text) -> Either ApiRequestError (EmbedPath, LogicTree)
pRequestLogicTree (k, v) = mapError $ (,) <$> embedPath <*> logicTree
where
path = parse pLogicPath ("failed to parser logic path (" ++ toS k ++ ")") $ toS k
embedPath = fst <$> path
op = snd <$> path
-- Concat op and v to make pLogicTree argument regular, in the form of "?and=and(.. , ..)" instead of "?and=(.. , ..)"
logicTree = join $ parse pLogicTree ("failed to parse logic tree (" ++ toS v ++ ")") . toS <$> ((<>) <$> op <*> pure v)
ws :: Parser Text
ws = toS <$> many (oneOf " \t")
lexeme :: Parser a -> Parser a
lexeme p = ws *> p <* ws
pTreePath :: Parser (EmbedPath, Field)
pTreePath = do
p <- pFieldName `sepBy1` pDelimiter
jp <- option [] pJsonPath
return (init p, (last p, jp))
pFieldForest :: Parser [Tree SelectItem]
pFieldForest = pFieldTree `sepBy1` lexeme (char ',')
where
pFieldTree :: Parser (Tree SelectItem)
pFieldTree = try (Node <$> pRelationSelect <*> between (char '(') (char ')') pFieldForest) <|>
Node <$> pFieldSelect <*> pure []
pStar :: Parser Text
pStar = toS <$> (string "*" $> ("*"::ByteString))
pFieldName :: Parser Text
pFieldName =
pQuotedValue <|>
intercalate "-" . map toS <$> (many1 (letter <|> digit <|> oneOf "_ ") `sepBy1` dash) <?>
"field name (* or [a..z0..9_])"
where
isDash :: GenParser Char st ()
isDash = try ( char '-' >> notFollowedBy (char '>') )
dash :: Parser Char
dash = isDash $> '-'
pJsonPath :: Parser JsonPath
pJsonPath = many pJsonOperation
where
pJsonOperation :: Parser JsonOperation
pJsonOperation = pJsonArrow <*> pJsonOperand
pJsonArrow =
try (string "->>" $> J2Arrow) <|>
try (string "->" $> JArrow)
pJsonOperand =
let pJKey = JKey . toS <$> pFieldName
pJIdx = JIdx . toS <$> ((:) <$> option '+' (char '-') <*> many1 digit) <* pEnd
pEnd = try (void $ lookAhead (string "->")) <|>
try (void $ lookAhead (string "::")) <|>
try eof in
try pJIdx <|> try pJKey
pField :: Parser Field
pField = lexeme $ (,) <$> pFieldName <*> option [] pJsonPath
aliasSeparator :: Parser ()
aliasSeparator = char ':' >> notFollowedBy (char ':')
pRelationSelect :: Parser SelectItem
pRelationSelect = lexeme $ try ( do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
relationDetail <- optionMaybe ( try( char '.' *> pFieldName ) )
return (fld, Nothing, alias, relationDetail)
)
pFieldSelect :: Parser SelectItem
pFieldSelect = lexeme $
try (
do
alias <- optionMaybe ( try(pFieldName <* aliasSeparator) )
fld <- pField
cast' <- optionMaybe (string "::" *> many letter)
return (fld, toS <$> cast', alias, Nothing)
)
<|> do
s <- pStar
return ((s, []), Nothing, Nothing, Nothing)
pOpExpr :: Parser SingleVal -> Parser OpExpr
pOpExpr pSVal = try ( string "not" *> pDelimiter *> (OpExpr True <$> pOperation)) <|> OpExpr False <$> pOperation
where
pOperation :: Parser Operation
pOperation =
Op . toS <$> foldl1 (<|>) (try . ((<* pDelimiter) . string) . toS <$> M.keys ops) <*> pSVal
<|> In <$> (try (string "in" *> pDelimiter) *> pListVal)
<|> pFts
<?> "operator (eq, gt, ...)"
pFts = do
op <- foldl1 (<|>) (try . string . toS <$> ftsOps)
lang <- optionMaybe $ try (between (char '(') (char ')') (many (letter <|> digit <|> oneOf "_")))
pDelimiter >> Fts (toS op) (toS <$> lang) <$> pSVal
ops = M.filterWithKey (const . flip notElem ("in":ftsOps)) operators
ftsOps = M.keys ftsOperators
pSingleVal :: Parser SingleVal
pSingleVal = toS <$> many anyChar
pListVal :: Parser ListVal
pListVal = lexeme (char '(') *> pListElement `sepBy1` char ',' <* lexeme (char ')')
pListElement :: Parser Text
pListElement = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> (toS <$> many (noneOf ",)"))
pQuotedValue :: Parser Text
pQuotedValue = toS <$> (char '"' *> many (noneOf "\"") <* char '"')
pDelimiter :: Parser Char
pDelimiter = char '.' <?> "delimiter (.)"
pOrder :: Parser [OrderTerm]
pOrder = lexeme pOrderTerm `sepBy1` char ','
pOrderTerm :: Parser OrderTerm
pOrderTerm = do
fld <- pField
dir <- optionMaybe $
try (pDelimiter *> string "asc" $> OrderAsc) <|>
try (pDelimiter *> string "desc" $> OrderDesc)
nls <- optionMaybe pNulls <* pEnd <|>
pEnd $> Nothing
return $ OrderTerm fld dir nls
where
pNulls = try (pDelimiter *> string "nullsfirst" $> OrderNullsFirst) <|>
try (pDelimiter *> string "nullslast" $> OrderNullsLast)
pEnd = try (void $ lookAhead (char ',')) <|>
try eof
pLogicTree :: Parser LogicTree
pLogicTree = Stmnt <$> try pLogicFilter
<|> Expr <$> pNot <*> pLogicOp <*> (lexeme (char '(') *> pLogicTree `sepBy1` lexeme (char ',') <* lexeme (char ')'))
where
pLogicFilter :: Parser Filter
pLogicFilter = Filter <$> pField <* pDelimiter <*> pOpExpr pLogicSingleVal
pNot :: Parser Bool
pNot = try (string "not" *> pDelimiter $> True)
<|> pure False
<?> "negation operator (not)"
pLogicOp :: Parser LogicOperator
pLogicOp = try (string "and" $> And)
<|> string "or" $> Or
<?> "logic operator (and, or)"
pLogicSingleVal :: Parser SingleVal
pLogicSingleVal = try (pQuotedValue <* notFollowedBy (noneOf ",)")) <|> try pPgArray <|> (toS <$> many (noneOf ",)"))
where
pPgArray :: Parser Text
pPgArray = do
a <- string "{"
b <- many (noneOf "{}")
c <- string "}"
toS <$> pure (a ++ b ++ c)
pLogicPath :: Parser (EmbedPath, Text)
pLogicPath = do
path <- pFieldName `sepBy1` pDelimiter
let op = last path
notOp = "not." <> op
return (filter (/= "not") (init path), if "not" `elem` path then notOp else op)
mapError :: Either ParseError a -> Either ApiRequestError a
mapError = mapLeft translateError
where
translateError e =
ParseRequestError message details
where
message = show $ errorPos e
details = strip $ replace "\n" " " $ toS
$ showErrorMessages "or" "unknown parse error" "expecting" "unexpected" "end of input" (errorMessages e)
-- Used for the config value "role-claim-key"
pRoleClaimKey :: Text -> Either ApiRequestError JSPath
pRoleClaimKey selStr =
mapError $ parse pJSPath ("failed to parse role-claim-key value (" <> toS selStr <> ")") (toS selStr)
pJSPath :: Parser JSPath
pJSPath = toJSPath <$> (period *> pPath `sepBy` period <* eof)
where
toJSPath :: [(Text, Maybe Int)] -> JSPath
toJSPath = concatMap (\(key, idx) -> JSPKey key : maybeToList (JSPIdx <$> idx))
period = char '.' <?> "period (.)"
pPath :: Parser (Text, Maybe Int)
pPath = (,) <$> pJSPKey <*> optionMaybe pJSPIdx
pJSPKey :: Parser Text
pJSPKey = toS <$> many1 (alphaNum <|> oneOf "_$@") <|> pQuotedValue <?> "attribute name [a..z0..9_$@])"
pJSPIdx :: Parser Int
pJSPIdx = char '[' *> (read <$> many1 digit) <* char ']' <?> "array index [0..n]"
-322
View File
@@ -1,322 +0,0 @@
{-# LANGUAGE TypeSynonymInstances, FlexibleInstances, MultiWayIf #-}
{-# OPTIONS_GHC -fno-warn-orphans #-}
module PostgREST.PgQuery where
import PostgREST.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 qualified Data.HashMap.Strict as H
import Text.Regex.TDFA ( (=~) )
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 qualified Data.Vector as V
import Data.Scientific (isInteger, formatScientific, FPFormat(..))
import Prelude
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 QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: T.Text
, qiName :: T.Text
} deriving (Show)
data OrderTerm = OrderTerm {
otTerm :: T.Text
, otDirection :: BS.ByteString
, otNullOrder :: Maybe 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 :: QualifiedIdentifier -> Net.Query -> StatementT
whereT table 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"] ]
wherePredTable = wherePred table
conjunction = mconcat $ L.intersperse andq (map wherePredTable cols)
withT :: PStmt -> T.Text -> StatementT
withT (B.Stmt eq ep epre) v (B.Stmt wq wp wpre) =
B.Stmt ("WITH " <> v <> " AS (" <> eq <> ") " <> wq <> " from " <> v)
(ep <> wp)
(epre && wpre)
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) <> " "
<> maybe "" cs (otNullOrder 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 pg_catalog.count(1) FROM qqq" }
countRows :: QualifiedIdentifier -> PStmt
countRows t = B.Stmt ("select pg_catalog.count(1) from " <> fromQi t) empty True
asCsvWithCount :: QualifiedIdentifier -> StatementT
asCsvWithCount table = withCount . asCsv table
asCsv :: QualifiedIdentifier -> StatementT
asCsv table s = s { B.stmtTemplate =
"(select string_agg(quote_ident(column_name::text), ',') from "
<> "(select column_name from information_schema.columns where quote_ident(table_schema) || '.' || table_name = '"
<> fromQi table <> "' order by ordinal_position) h) || '\r' || "
<> "coalesce(string_agg(substring(t::text, 2, length(t::text) - 2), '\r'), '') from ("
<> B.stmtTemplate s <> ") t" }
asJsonWithCount :: StatementT
asJsonWithCount = withCount . asJson
asJson :: StatementT
asJson s = s { B.stmtTemplate =
"array_to_json(array_agg(row_to_json(t)))::character varying from ("
<> B.stmtTemplate s <> ") t" }
withCount :: StatementT
withCount s = s { B.stmtTemplate = "pg_catalog.count(t), " <> B.stmtTemplate s }
asJsonRow :: StatementT
asJsonRow s = s { B.stmtTemplate = "row_to_json(t) from (" <> B.stmtTemplate s <> ") t" }
selectStar :: QualifiedIdentifier -> PStmt
selectStar t = B.Stmt ("select * from " <> fromQi t) empty True
returningStarT :: StatementT
returningStarT s = s { B.stmtTemplate = B.stmtTemplate s <> " RETURNING *" }
deleteFrom :: QualifiedIdentifier -> PStmt
deleteFrom t = B.Stmt ("delete from " <> fromQi t) empty True
insertInto :: QualifiedIdentifier
-> V.Vector T.Text
-> V.Vector (V.Vector JSON.Value)
-> PStmt
insertInto t cols vals
| V.null cols = B.Stmt ("insert into " <> fromQi t <> " default values returning *") empty True
| otherwise = B.Stmt
("insert into " <> fromQi t <> " (" <>
T.intercalate ", " (V.toList $ V.map pgFmtIdent cols) <>
") values "
<> T.intercalate ", "
(V.toList $ V.map (\v -> "("
<> T.intercalate ", " (V.toList $ V.map insertableValue v)
<> ")"
) vals
)
<> " returning row_to_json(" <> fromQi t <> ".*)")
empty True
insertSelect :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
insertSelect t [] _ = B.Stmt
("insert into " <> fromQi t <> " default values returning *") empty True
insertSelect t cols vals = B.Stmt
("insert into " <> fromQi t <> " ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") select "
<> T.intercalate ", " (map insertableValue vals))
empty True
update :: QualifiedIdentifier -> [T.Text] -> [JSON.Value] -> PStmt
update t cols vals = B.Stmt
("update " <> fromQi t <> " set ("
<> T.intercalate ", " (map pgFmtIdent cols)
<> ") = ("
<> T.intercalate ", " (map insertableValue vals)
<> ")")
empty True
callProc :: QualifiedIdentifier -> JSON.Object -> PStmt
callProc qi params = do
let args = T.intercalate "," $ map assignment (H.toList params)
B.Stmt ("select * from " <> fromQi qi <> "(" <> args <> ")") empty True
where
assignment (n,v) = pgFmtIdent n <> ":=" <> insertableValue v
wherePred :: QualifiedIdentifier -> Net.QueryItem -> PStmt
wherePred table (col, predicate) =
B.Stmt (notOp <> " " <> pgFmtJsonbPath table (cs col) <> " " <> op <> " " <>
if opCode `elem` ["is","isnot"] then whiteList value
else cs sqlValue)
empty True
where
headPredicate:rest = T.split (=='.') $ cs $ fromMaybe "." predicate
hasNot caseTrue caseFalse = if headPredicate == "not" then caseTrue else caseFalse
opCode = hasNot (head rest) headPredicate
notOp = hasNot headPredicate ""
value = hasNot (T.intercalate "." $ tail rest) (T.intercalate "." rest)
whiteList val = fromMaybe (cs (pgFmtLit val) <> "::unknown ")
(L.find ((==) . T.toLower $ val)
["null","true","false"])
star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit
sqlValue = case opCode of
"like" -> unknownLiteral $ T.map star value
"ilike" -> unknownLiteral $ T.map star value
"in" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"notin" -> "(" <> T.intercalate ", " (map unknownLiteral $ T.split (==',') value) <> ") "
"@@" -> "to_tsquery(" <> unknownLiteral value <> ") "
_ -> unknownLiteral value
op = case opCode of
"eq" -> "="
"gt" -> ">"
"lt" -> "<"
"gte" -> ">="
"lte" -> "<="
"neq" -> "<>"
"like"-> "like"
"ilike"-> "ilike"
"in" -> "in"
"notin" -> "not in"
"is" -> "is"
"isnot" -> "is not"
"@@" -> "@@"
_ -> "="
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:nls) ->
if d `elem` ["asc", "desc"]
then Just $ OrderTerm c
( if d == "asc" then "asc" else "desc" )
( case nls of
[n] -> if | n == "nullsfirst" -> Just "nulls first"
| n == "nullslast" -> Just "nulls last"
| otherwise -> Nothing
_ -> Nothing
)
else Nothing
_ -> Nothing
commaq :: PStmt
commaq = B.Stmt ", " empty True
andq :: PStmt
andq = B.Stmt " and " empty True
data JsonbPath =
ColIdentifier T.Text
| KeyIdentifier T.Text
| SingleArrow JsonbPath JsonbPath
| DoubleArrow JsonbPath JsonbPath
deriving (Show)
parseJsonbPath :: T.Text -> Maybe JsonbPath
parseJsonbPath p =
case T.splitOn "->>" p of
[a,b] ->
let i:is = T.splitOn "->" a in
Just $ DoubleArrow
(foldl SingleArrow (ColIdentifier i) (map KeyIdentifier is))
(KeyIdentifier b)
_ -> Nothing
pgFmtJsonbPath :: QualifiedIdentifier -> T.Text -> T.Text
pgFmtJsonbPath table p =
pgFmtJsonbPath' $ fromMaybe (ColIdentifier p) (parseJsonbPath p)
where
pgFmtJsonbPath' (ColIdentifier i) = fromQi table <> "." <> pgFmtIdent i
pgFmtJsonbPath' (KeyIdentifier i) = pgFmtLit i
pgFmtJsonbPath' (SingleArrow a b) =
pgFmtJsonbPath' a <> "->" <> pgFmtJsonbPath' b
pgFmtJsonbPath' (DoubleArrow a b) =
pgFmtJsonbPath' a <> "->>" <> pgFmtJsonbPath' b
pgFmtIdent :: T.Text -> T.Text
pgFmtIdent x =
let escaped = T.replace "\"" "\"\"" (trimNullChars $ cs x) in
if (cs escaped :: BS.ByteString) =~ danger
then "\"" <> escaped <> "\""
else escaped
where danger = "^$|^[^a-z_]|[^a-z_0-9]" :: BS.ByteString
pgFmtLit :: T.Text -> T.Text
pgFmtLit x =
let trimmed = trimNullChars x
escaped = "'" <> T.replace "'" "''" trimmed <> "'"
slashed = T.replace "\\" "\\\\" escaped in
if T.isInfixOf "\\\\" escaped
then "E" <> slashed
else slashed
trimNullChars :: T.Text -> T.Text
trimNullChars = T.takeWhile (/= '\x0')
fromQi :: QualifiedIdentifier -> T.Text
fromQi t = pgFmtIdent (qiSchema t) <> "." <> pgFmtIdent (qiName 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 v = cs $ JSON.encode v
insertableText :: T.Text -> T.Text
insertableText = (<> "::unknown") . pgFmtLit
insertableValue :: JSON.Value -> T.Text
insertableValue JSON.Null = "null"
insertableValue v = insertableText $ unquoted v
paramFilter :: JSON.Value -> T.Text
paramFilter JSON.Null = "is.null"
paramFilter v = "eq." <> unquoted v
-198
View File
@@ -1,198 +0,0 @@
{-# LANGUAGE QuasiQuotes, OverloadedStrings, TypeSynonymInstances,
MultiParamTypeClasses, ScopedTypeVariables,
FlexibleContexts #-}
module PostgREST.PgStructure where
import PostgREST.PgQuery (QualifiedIdentifier(..))
import Data.Text hiding (foldl, map, zipWith, concat)
import Data.Aeson
import Data.Functor.Identity
import Data.String.Conversions (cs)
import Data.Maybe (fromMaybe, isJust)
import Control.Applicative
import qualified Data.Map as Map
import qualified Hasql as H
import qualified Hasql.Postgres as P
import Prelude
foreignKeys :: QualifiedIdentifier -> 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
|] (qiName table) (qiSchema 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
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 = ?
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
|] schema
return $ map tableFromRow rows
columns :: QualifiedIdentifier -> 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::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 (
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 |]
(qiSchema table) (qiName table)
fks <- foreignKeys table
return $ map (addFK fks . columnFromRow) cols
where
addFK fks col = col { colFK = Map.lookup (cs . colName $ col) fks }
primaryKeyColumns :: QualifiedIdentifier -> 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 = ? |] (qiSchema table) (qiName table)
return $ map runIdentity r
doesProcExist :: Text -> Text -> H.Tx P.Postgres s Bool
doesProcExist schema proc = do
row :: Maybe (Identity Int) <- H.maybeEx $ [H.stmt|
SELECT 1
FROM pg_catalog.pg_namespace n
JOIN pg_catalog.pg_proc p
ON pronamespace = n.oid
WHERE nspname = ?
AND proname = ?
|] schema proc
return $ isJust row
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, Bool) -> Table
tableFromRow (s, n, i) = Table s n i
columnFromRow :: (Text, Text, Text,
Int, Bool, Text,
Bool, 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 nul typ 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 ]
+481
View File
@@ -0,0 +1,481 @@
{-# LANGUAGE FlexibleInstances #-}
{-# LANGUAGE FlexibleContexts #-}
{-# LANGUAGE DuplicateRecordFields #-}
{-# LANGUAGE LambdaCase #-}
{-# 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 (
callProc
, createReadStatement
, createWriteStatement
, pgFmtIdent
, pgFmtLit
, requestToQuery
, requestToCountQuery
, unquoted
, ResultsWithCount
, pgFmtSetLocal
, pgFmtSetLocalSearchPath
) where
import qualified Hasql.Statement as H
import qualified Hasql.Encoders as HE
import qualified Hasql.Decoders as HD
import qualified Data.Aeson as JSON
import PostgREST.RangeQuery (rangeLimit, rangeOffset, allRange)
import qualified Data.HashMap.Strict as HM
import Data.Maybe
import qualified Data.Set as S
import Data.Text (intercalate, unwords, replace, isInfixOf, toLower)
import qualified Data.Text as T (map, takeWhile, null)
import qualified Data.Text.Encoding as T
import Data.Tree (Tree(..))
import PostgREST.Types
import Text.InterpolatedString.Perl6 (qc)
import qualified Data.ByteString.Char8 as BS
import Data.Scientific ( FPFormat (..)
, formatScientific
, isInteger
)
import Protolude hiding ( intercalate, cast, replace)
import PostgREST.ApiRequest (PreferRepresentation (..))
{-| The generic query result format used by API responses. The location header
is represented as a list of strings containing variable bindings like
@"k1=eq.42"@, or the empty list if there is no location header.
-}
type ResultsWithCount = (Maybe Int64, Int64, [BS.ByteString], BS.ByteString)
standardRow :: HD.Row ResultsWithCount
standardRow = (,,,) <$> HD.nullableColumn HD.int8 <*> HD.column HD.int8
<*> HD.column header <*> HD.column HD.bytea
where
header = HD.array $ HD.dimension replicateM $ HD.element HD.bytea
noLocationF :: Text
noLocationF = "array[]::text[]"
{-| 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
decodeStandardMay :: HD.Result (Maybe ResultsWithCount)
decodeStandardMay =
HD.rowMaybe standardRow
createReadStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool -> Maybe FieldName ->
H.Statement () ResultsWithCount
createReadStatement selectQuery countQuery isSingle countTotal asCsv binaryField =
unicodeStatement sql HE.unit decodeStandard False
where
sql = [qc|
WITH {sourceCTEName} AS ({selectQuery}) SELECT {cols}
FROM ( SELECT * FROM {sourceCTEName}) _postgrest_t |]
countResultF = if countTotal then "("<>countQuery<>")" else "null"
cols = intercalate ", " [
countResultF <> " AS total_result_set",
"pg_catalog.count(_postgrest_t) AS page_total",
noLocationF <> " AS header",
bodyF <> " AS body"
]
bodyF
| asCsv = asCsvF
| isSingle = asJsonSingleF
| isJust binaryField = asBinaryF $ fromJust binaryField
| otherwise = asJsonF
createWriteStatement :: SqlQuery -> SqlQuery -> Bool -> Bool -> Bool ->
PreferRepresentation -> [Text] ->
H.Statement ByteString (Maybe ResultsWithCount)
createWriteStatement selectQuery mutateQuery wantSingle wantHdrs asCsv rep pKeys =
unicodeStatement sql (HE.param HE.unknown) decodeStandardMay True
where
sql = case rep of
None -> [qc|
WITH {sourceCTEName} AS ({mutateQuery})
SELECT '', 0, {noLocationF}, '' |]
HeadersOnly -> [qc|
WITH {sourceCTEName} AS ({mutateQuery})
SELECT {cols}
FROM (SELECT 1 FROM {sourceCTEName}) _postgrest_t |]
Full -> [qc|
WITH {sourceCTEName} AS ({mutateQuery})
SELECT {cols}
FROM ({selectQuery}) _postgrest_t |]
cols = intercalate ", " [
"'' AS total_result_set", -- when updateing it does not make sense
"pg_catalog.count(_postgrest_t) AS page_total",
if wantHdrs
then "coalesce(" <> locationF pKeys <> ", " <> noLocationF <> ")"
else noLocationF <> " AS header",
if rep == Full
then bodyF <> " AS body"
else "''"
]
bodyF
| asCsv = asCsvF
| wantSingle = asJsonSingleF
| otherwise = asJsonF
type ProcResults = (Maybe Int64, Int64, ByteString, ByteString)
callProc :: QualifiedIdentifier -> [PgArg] -> Bool -> SqlQuery -> SqlQuery -> Bool ->
Bool -> Bool -> Bool -> Bool -> Maybe FieldName -> Bool -> PgVersion ->
H.Statement ByteString (Maybe ProcResults)
callProc qi pgArgs returnsScalar selectQuery countQuery countTotal isSingle paramsAsSingleObject asCsv asBinary binaryField isObject pgVer =
unicodeStatement sql (HE.param HE.unknown) decodeProc True
where
sql =
if returnsScalar then [qc|
WITH {argsRecord},
{sourceCTEName} AS (
SELECT {fromQi qi}({args})
)
SELECT
{countResultF} AS total_result_set,
1 AS page_total,
{scalarBodyF} AS body,
{responseHeaders} AS response_headers
FROM ({selectQuery}) _postgrest_t;|]
else [qc|
WITH {argsRecord},
{sourceCTEName} AS (
SELECT * FROM {fromQi qi}({args})
)
SELECT
{countResultF} AS total_result_set,
pg_catalog.count(_postgrest_t) AS page_total,
{bodyF} AS body,
{responseHeaders} AS response_headers
FROM ({selectQuery}) _postgrest_t;|]
(argsRecord, args) | paramsAsSingleObject = ("_args_record AS (SELECT NULL)", "$1::json")
| null pgArgs = (ignoredBody, "")
| otherwise = (
unwords [
"_args_record AS (",
"SELECT * FROM " <> (if isObject then "json_to_record" else "json_to_recordset") <> "($1)",
"AS _(" <> intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " " <> pgaType a) <$> pgArgs) <> ")",
")"]
, intercalate ", " ((\a -> pgFmtIdent (pgaName a) <> " := (SELECT " <> pgFmtIdent (pgaName a) <> " FROM _args_record)") <$> pgArgs))
countResultF = if countTotal then "( "<> countQuery <> ")" else "null::bigint" :: Text
_procName = qiName qi
responseHeaders =
if pgVer >= pgVersion96
then "coalesce(nullif(current_setting('response.headers', true), ''), '[]')" :: Text -- nullif is used because of https://gist.github.com/steve-chavez/8d7033ea5655096903f3b52f8ed09a15
else "'[]'" :: Text
decodeProc = HD.rowMaybe procRow
procRow = (,,,) <$> HD.nullableColumn HD.int8 <*> HD.column HD.int8
<*> HD.column HD.bytea <*> HD.column HD.bytea
scalarBodyF
| asBinary = asBinaryF _procName
| otherwise = "(row_to_json(_postgrest_t)->" <> pgFmtLit _procName <> ")::character varying"
bodyF
| isSingle = asJsonSingleF
| asCsv = asCsvF
| isJust binaryField = asBinaryF $ fromJust binaryField
| otherwise = asJsonF
pgFmtIdent :: SqlFragment -> SqlFragment
pgFmtIdent x = "\"" <> replace "\"" "\"\"" (trimNullChars $ toS 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 _) = witness
requestToCountQuery schema (DbRead (Node (Select{where_=logicForest}, (mainTbl, _, _, _, _)) _)) =
unwords [
"SELECT pg_catalog.count(*)",
"FROM ", fromQi qi,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest
]
where
qi = removeSourceCTESchema schema mainTbl
requestToQuery :: Schema -> Bool -> DbRequest -> SqlQuery
requestToQuery schema isParent (DbRead (Node (Select colSelects tbls logicForest joinConditions_ ordts range, (nodeName, maybeRelation, _, _, depth)) forest)) =
unwords [
"SELECT " <> intercalate ", " (map (pgFmtSelectItem qi) colSelects ++ selects),
"FROM " <> intercalate ", " tables,
unwords joins,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest ++ map pgFmtJoinCondition joinConds))
`emptyOnFalse` (null logicForest && null joinConds),
("ORDER BY " <> intercalate ", " (map (pgFmtOrderTerm qi) ordts)) `emptyOnFalse` null ordts,
("LIMIT " <> maybe "ALL" show (rangeLimit range) <> " OFFSET " <> show (rangeOffset range)) `emptyOnFalse` (isParent || range == allRange) ]
where
mainTbl = maybe nodeName (tableName . relTable) maybeRelation
isSelfJoin = maybe False (\r -> relType r /= Root && relTable r == relFTable r) maybeRelation
(qi, tables, joinConds) =
let depthAlias name dpth = if dpth /= 0 then name <> "_" <> show dpth else name in -- Root node doesn't get aliased
if isSelfJoin
then (
QualifiedIdentifier "" (depthAlias mainTbl depth),
(\t -> fromQi (removeSourceCTESchema schema t) <> " AS " <> pgFmtIdent (depthAlias t depth)) <$> tbls,
(\(JoinCondition (qi1, _, c1) (qi2, _, c2)) ->
JoinCondition (qi1, Just $ depthAlias (qiName qi1) depth, c1)
(qi2, Just $ depthAlias (qiName qi2) (depth - 1), c2)) <$> joinConditions_)
else (
removeSourceCTESchema schema mainTbl,
fromQi . removeSourceCTESchema schema <$> tbls,
joinConditions_)
(joins, selects) = foldr getQueryParts ([],[]) forest
getQueryParts :: Tree ReadNode -> ([SqlFragment], [SqlFragment]) -> ([SqlFragment], [SqlFragment])
getQueryParts (Node n@(_, (name, Just Relation{relType=Child,relTable=Table{tableName=table}}, alias, _, _)) forst) (j,s) = (j,sel:s)
where
sel = "COALESCE(("
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
where subquery = requestToQuery schema False (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just Relation{relType=Parent,relTable=Table{tableName=table}}, alias, _, _)) forst) (j,s) = (joi:j,sel:s)
where
aliasOrName = fromMaybe name alias
localTableName = pgFmtIdent $ table <> "_" <> aliasOrName
sel = "row_to_json(" <> localTableName <> ".*) AS " <> pgFmtIdent aliasOrName
joi = " LEFT JOIN LATERAL( " <> subquery <> " ) AS " <> localTableName <> " ON TRUE "
where subquery = requestToQuery schema True (DbRead (Node n forst))
getQueryParts (Node n@(_, (name, Just Relation{relType=Many,relTable=Table{tableName=table}}, alias, _, _)) forst) (j,s) = (j,sel:s)
where
sel = "COALESCE (("
<> "SELECT json_agg(" <> pgFmtIdent table <> ".*) "
<> "FROM (" <> subquery <> ") " <> pgFmtIdent table
<> "), '[]') AS " <> pgFmtIdent (fromMaybe name alias)
where subquery = requestToQuery schema False (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 _ _ = witness
requestToQuery schema _ (DbMutate (Insert mainTbl pkCols p@(PayloadJSON _ pType pKeys) onConflct logicForest returnings)) =
unwords [
("WITH " <> ignoredBody) `emptyOnFalse` not payloadIsEmpty,
"INSERT INTO ", fromQi qi, if payloadIsEmpty then " " else "(" <> cols <> ")",
case (pType, payloadIsEmpty) of
(PJArray _, True) -> "SELECT null WHERE false"
(PJObject, True) -> "DEFAULT VALUES"
_ -> unwords [
"SELECT " <> cols <> " FROM",
case pType of
PJObject -> "json_populate_record"
PJArray _ -> "json_populate_recordset", "(null::", fromQi qi, ", $1) _",
-- Only used for PUT
("WHERE " <> intercalate " AND " (pgFmtLogicTree (QualifiedIdentifier "" "_") <$> logicForest)) `emptyOnFalse` null logicForest],
maybe "" (\x -> (
"ON CONFLICT(" <> intercalate ", " (pgFmtIdent <$> pkCols) <> ") " <> case x of
IgnoreDuplicates ->
"DO NOTHING"
MergeDuplicates ->
"DO UPDATE SET " <> intercalate ", " (pgFmtIdent <> const " = EXCLUDED." <> pgFmtIdent <$> S.toList pKeys)
) `emptyOnFalse` null pkCols) onConflct,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings]
where
qi = QualifiedIdentifier schema mainTbl
cols = intercalate ", " $ pgFmtIdent <$> S.toList pKeys
payloadIsEmpty = pjIsEmpty p
requestToQuery schema _ (DbMutate (Update mainTbl p@(PayloadJSON _ pType keys) logicForest returnings)) =
if pjIsEmpty p
then "WITH " <> ignoredBody <> "SELECT ''"
else
unwords [
"UPDATE " <> fromQi qi <> " SET " <> cols,
"FROM (SELECT * FROM ",
case pType of
PJObject -> " json_populate_record"
PJArray _ -> " json_populate_recordset", "(null::", fromQi qi, ", $1)) _ ",
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings
]
where
qi = QualifiedIdentifier schema mainTbl
cols = intercalate ", " (pgFmtIdent <> const " = _." <> pgFmtIdent <$> S.toList keys)
requestToQuery schema _ (DbMutate (Delete mainTbl logicForest returnings)) =
unwords [
"WITH " <> ignoredBody,
"DELETE FROM ", fromQi qi,
("WHERE " <> intercalate " AND " (map (pgFmtLogicTree qi) logicForest)) `emptyOnFalse` null logicForest,
("RETURNING " <> intercalate ", " (map (pgFmtColumn qi) returnings)) `emptyOnFalse` null returnings
]
where
qi = QualifiedIdentifier schema mainTbl
-- Due to the use of the `unknown` encoder we need to cast '$1' when the value is not used in the main query
-- otherwise the query will err with a `could not determine data type of parameter $1`.
-- This happens because `unknown` relies on the context to determine the value type.
-- The error also happens on raw libpq used with C.
ignoredBody :: SqlFragment
ignoredBody = "ignored_body AS (SELECT $1::text) "
removeSourceCTESchema :: Schema -> TableName -> QualifiedIdentifier
removeSourceCTESchema schema tbl = QualifiedIdentifier (if tbl == sourceCTEName then "" else schema) tbl
unquoted :: JSON.Value -> Text
unquoted (JSON.String t) = t
unquoted (JSON.Number n) =
toS $ formatScientific Fixed (if isInteger n then Just 0 else Nothing) n
unquoted (JSON.Bool b) = show b
unquoted v = toS $ 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(_postgrest_t::text, 2, length(_postgrest_t::text) - 2), '\n'), '')"
asJsonF :: SqlFragment
asJsonF = "coalesce(json_agg(_postgrest_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(_postgrest_t)::text, ','), '')::character varying "
asBinaryF :: FieldName -> SqlFragment
asBinaryF fieldName = "coalesce(string_agg(_postgrest_t." <> pgFmtIdent fieldName <> ", ''), '')"
locationF :: [Text] -> SqlFragment
locationF pKeys = [qc|(
WITH data AS (SELECT row_to_json(_) AS row FROM {sourceCTEName} AS _ LIMIT 1)
SELECT array_agg(json_data.key || '=' || coalesce('eq.' || json_data.value, 'is.null'))
FROM data CROSS JOIN json_each_text(data.row) AS json_data
{("WHERE json_data.key IN ('" <> intercalate "','" pKeys <> "')") `emptyOnFalse` null pKeys}
)|]
fromQi :: QualifiedIdentifier -> SqlFragment
fromQi t = (if s == "" then "" else pgFmtIdent s <> ".") <> pgFmtIdent n
where
n = qiName t
s = qiSchema t
unicodeStatement :: Text -> HE.Params a -> HD.Result b -> Bool -> H.Statement a b
unicodeStatement = H.Statement . T.encodeUtf8
emptyOnFalse :: Text -> Bool -> Text
emptyOnFalse val cond = if cond then "" else val
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@(fName, jp), Nothing, alias, _) = pgFmtField table f <> pgFmtAs fName jp alias
pgFmtSelectItem table (f@(fName, jp), Just cast, alias, _) = "CAST (" <> pgFmtField table f <> " AS " <> cast <> " )" <> pgFmtAs fName jp alias
pgFmtOrderTerm :: QualifiedIdentifier -> OrderTerm -> SqlFragment
pgFmtOrderTerm qi ot = unwords [
toS . pgFmtField qi $ otTerm ot,
maybe "" show $ otDirection ot,
maybe "" show $ otNullOrder ot]
pgFmtFilter :: QualifiedIdentifier -> Filter -> SqlFragment
pgFmtFilter table (Filter fld (OpExpr hasNot oper)) = notOp <> " " <> case oper of
Op op val -> pgFmtFieldOp op <> " " <> case op of
"like" -> unknownLiteral (T.map star val)
"ilike" -> unknownLiteral (T.map star val)
"is" -> whiteList val
_ -> unknownLiteral val
In vals -> pgFmtField table fld <> " " <>
let emptyValForIn = "= any('{}') " in -- Workaround because for postgresql "col IN ()" is invalid syntax, we instead do "col = any('{}')"
case (&&) (length vals == 1) . T.null <$> headMay vals of
Just False -> sqlOperator "in" <> "(" <> intercalate ", " (map unknownLiteral vals) <> ") "
Just True -> emptyValForIn
Nothing -> emptyValForIn
Fts op lang val ->
pgFmtFieldOp op
<> "("
<> maybe "" ((<> ", ") . pgFmtLit) lang
<> unknownLiteral val
<> ") "
where
pgFmtFieldOp op = pgFmtField table fld <> " " <> sqlOperator op
sqlOperator o = HM.lookupDefault "=" o operators
notOp = if hasNot then "NOT" else ""
star c = if c == '*' then '%' else c
unknownLiteral = (<> "::unknown ") . pgFmtLit
whiteList :: Text -> SqlFragment
whiteList v = fromMaybe
(toS (pgFmtLit v) <> "::unknown ")
(find ((==) . toLower $ v) ["null","true","false"])
pgFmtJoinCondition :: JoinCondition -> SqlFragment
pgFmtJoinCondition (JoinCondition (qi, al1, col1) (QualifiedIdentifier schema fTable, al2, col2)) =
pgFmtColumn (fromMaybe qi $ aliasToQi al1) col1 <> " = " <>
pgFmtColumn (fromMaybe (removeSourceCTESchema schema fTable) $ aliasToQi al2) col2
where
aliasToQi al = QualifiedIdentifier "" <$> al
pgFmtLogicTree :: QualifiedIdentifier -> LogicTree -> SqlFragment
pgFmtLogicTree qi (Expr hasNot op forest) = notOp <> " (" <> intercalate (" " <> show op <> " ") (pgFmtLogicTree qi <$> forest) <> ")"
where notOp = if hasNot then "NOT" else ""
pgFmtLogicTree qi (Stmnt flt) = pgFmtFilter qi flt
pgFmtJsonPath :: JsonPath -> SqlFragment
pgFmtJsonPath = \case
[] -> ""
(JArrow x:xs) -> "->" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
(J2Arrow x:xs) -> "->>" <> pgFmtJsonOperand x <> pgFmtJsonPath xs
where
pgFmtJsonOperand (JKey k) = pgFmtLit k
pgFmtJsonOperand (JIdx i) = pgFmtLit i <> "::int"
pgFmtAs :: FieldName -> JsonPath -> Maybe Alias -> SqlFragment
pgFmtAs _ [] Nothing = ""
pgFmtAs fName jp Nothing = case jOp <$> lastMay jp of
Just (JKey key) -> " AS " <> pgFmtIdent key
Just (JIdx _) -> " AS " <> pgFmtIdent (fromMaybe fName lastKey)
-- We get the lastKey because on:
-- `select=data->1->mycol->>2`, we need to show the result as [ {"mycol": ..}, {"mycol": ..} ]
-- `select=data->3`, we need to show the result as [ {"data": ..}, {"data": ..} ]
where lastKey = jVal <$> find (\case JKey{} -> True; _ -> False) (jOp <$> reverse jp)
Nothing -> ""
pgFmtAs _ _ (Just alias) = " AS " <> pgFmtIdent alias
pgFmtSetLocal :: Text -> (Text, Text) -> SqlFragment
pgFmtSetLocal prefix (k, v) =
"SET LOCAL " <> pgFmtIdent (prefix <> k) <> " = " <> pgFmtLit v <> ";"
pgFmtSetLocalSearchPath :: [Text] -> SqlFragment
pgFmtSetLocalSearchPath vals =
"SET LOCAL search_path = " <> intercalate ", " (pgFmtLit <$> vals) <> ";"
trimNullChars :: Text -> Text
trimNullChars = T.takeWhile (/= '\x0')
+40 -30
View File
@@ -3,58 +3,68 @@ module PostgREST.RangeQuery (
, rangeRequested
, rangeLimit
, rangeOffset
, restrictRange
, rangeGeq
, allRange
, NonnegRange
) where
import Control.Applicative
import Network.HTTP.Types.Header
import qualified Data.ByteString.Char8 as BS
import Control.Applicative
import Network.HTTP.Types.Header
import Data.Ranged.Boundaries
import Data.Ranged.Ranges
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 Text.Regex.TDFA ((=~))
import Data.Maybe (fromMaybe, listToMaybe)
import Data.List (lookup)
import Prelude
import Protolude
type NonnegRange = Range Int
type NonnegRange = Range Integer
rangeParse :: BS.ByteString -> Maybe NonnegRange
rangeParse :: BS.ByteString -> NonnegRange
rangeParse range = do
let rangeRegex = "^([0-9]+)-([0-9]*)$" :: BS.ByteString
parsedRange <- listToMaybe (range =~ rangeRegex :: [[BS.ByteString]])
case listToMaybe (range =~ rangeRegex :: [[BS.ByteString]]) of
Just parsedRange ->
let [_, mLower, mUpper] = readMaybe . toS <$> parsedRange
lower = maybe emptyRange rangeGeq mLower
upper = maybe allRange rangeLeq mUpper in
rangeIntersection lower upper
Nothing -> allRange
let [_, from, to] = readMaybe . cs <$> parsedRange
let lower = fromMaybe emptyRange (rangeGeq <$> from)
let upper = fromMaybe (rangeGeq 0) (rangeLeq <$> to)
rangeRequested :: RequestHeaders -> NonnegRange
rangeRequested headers = maybe allRange rangeParse $ lookup hRange headers
return $ rangeIntersection lower upper
restrictRange :: Maybe Integer -> NonnegRange -> NonnegRange
restrictRange Nothing r = r
restrictRange (Just limit) r =
rangeIntersection r $
Range BoundaryBelowAll (BoundaryAbove $ rangeOffset r + limit - 1)
rangeRequested :: RequestHeaders -> Maybe NonnegRange
rangeRequested = (rangeParse =<<) . lookup hRange
rangeLimit :: NonnegRange -> Maybe Int
rangeLimit :: NonnegRange -> Maybe Integer
rangeLimit range =
case [rangeLower range, rangeUpper range]
of [BoundaryBelow from, BoundaryAbove to] -> Just (1 + to - from)
_ -> Nothing
case [rangeLower range, rangeUpper range] of
[BoundaryBelow lower, BoundaryAbove upper] -> Just (1 + upper - lower)
_ -> Nothing
rangeOffset :: NonnegRange -> Int
rangeOffset :: NonnegRange -> Integer
rangeOffset range =
case rangeLower range
of BoundaryBelow from -> from
_ -> error "range without lower bound" -- should never happen
case rangeLower range of
BoundaryBelow lower -> lower
_ -> panic "range without lower bound" -- should never happen
rangeGeq :: Int -> NonnegRange
rangeGeq :: Integer -> NonnegRange
rangeGeq n =
Range (BoundaryBelow n) BoundaryAboveAll
rangeLeq :: Int -> NonnegRange
allRange :: NonnegRange
allRange = rangeGeq 0
rangeLeq :: Integer -> NonnegRange
rangeLeq n =
Range BoundaryBelowAll (BoundaryAbove n)
+347
View File
@@ -0,0 +1,347 @@
{-# LANGUAGE DuplicateRecordFields #-}
module PostgREST.Types where
import Protolude
import qualified GHC.Show
import qualified Data.Aeson as JSON
import qualified Data.ByteString.Lazy as BL
import qualified Data.CaseInsensitive as CI
import qualified Data.HashMap.Strict as M
import qualified Data.Set as S
import Data.Tree
import PostgREST.RangeQuery (NonnegRange)
import Network.HTTP.Types.Header (hContentType, Header)
-- | Enumeration of currently supported response content types
data ContentType = CTApplicationJSON | CTTextCSV | CTOpenAPI
| CTSingularJSON | CTOctetStream
| CTAny | CTOther ByteString deriving Eq
data ApiRequestError = ActionInappropriate
| InvalidBody ByteString
| InvalidRange
| ParseRequestError Text Text
| UnknownRelation
| NoRelationBetween Text Text
| UnsupportedVerb
| InvalidFilters
deriving (Show, Eq)
data PreferResolution = MergeDuplicates | IgnoreDuplicates deriving Eq
instance Show PreferResolution where
show MergeDuplicates = "resolution=merge-duplicates"
show IgnoreDuplicates = "resolution=ignore-duplicates"
data DbStructure = DbStructure {
dbTables :: [Table]
, dbColumns :: [Column]
, dbRelations :: [Relation]
, dbPrimaryKeys :: [PrimaryKey]
-- ProcDescription is a list because a function can be overloaded
, dbProcs :: M.HashMap Text [ProcDescription]
, pgVersion :: PgVersion
} deriving (Show, Eq)
-- TODO Table could hold references to all its Columns
tableCols :: DbStructure -> Schema -> TableName -> [Column]
tableCols dbs tSchema tName = filter (\Column{colTable=Table{tableSchema=s, tableName=t}} -> s==tSchema && t==tName) $ dbColumns dbs
-- TODO Table could hold references to all its PrimaryKeys
tablePKCols :: DbStructure -> Schema -> TableName -> [Text]
tablePKCols dbs tSchema tName = pkName <$> filter (\pk -> tSchema == (tableSchema . pkTable) pk && tName == (tableName . pkTable) pk) (dbPrimaryKeys dbs)
data PgArg = PgArg {
pgaName :: Text
, pgaType :: Text
, pgaReq :: Bool
} deriving (Show, Eq, Ord)
data PgType = Scalar QualifiedIdentifier | Composite QualifiedIdentifier deriving (Eq, Show, Ord)
data RetType = Single PgType | SetOf PgType deriving (Eq, Show, Ord)
data ProcVolatility = Volatile | Stable | Immutable
deriving (Eq, Show, Ord)
data ProcDescription = ProcDescription {
pdName :: Text
, pdDescription :: Maybe Text
, pdArgs :: [PgArg]
, pdReturnType :: RetType
, pdVolatility :: ProcVolatility
} deriving (Show, Eq)
-- Order by least number of args in the case of overloaded functions
instance Ord ProcDescription where
ProcDescription name1 des1 args1 rt1 vol1 `compare` ProcDescription name2 des2 args2 rt2 vol2
| name1 == name2 && length args1 < length args2 = LT
| name1 == name2 && length args1 > length args2 = GT
| otherwise = (name1, des1, args1, rt1, vol1) `compare` (name2, des2, args2, rt2, vol2)
type Schema = Text
type TableName = Text
type SqlQuery = Text
type SqlFragment = Text
data Table = Table {
tableSchema :: Schema
, tableName :: TableName
, tableDescription :: Maybe Text
, tableInsertable :: Bool
} deriving (Show, Ord)
newtype ForeignKey = ForeignKey { fkCol :: Column } deriving (Show, Eq, Ord)
data Column =
Column {
colTable :: Table
, colName :: Text
, colDescription :: Maybe Text
, colPosition :: Int32
, colNullable :: Bool
, colType :: Text
, colUpdatable :: Bool
, colMaxLen :: Maybe Int32
, colPrecision :: Maybe Int32
, colDefault :: Maybe Text
, colEnum :: [Text]
, colFK :: Maybe ForeignKey
} deriving (Show, Ord)
-- | A view column that refers to a table column
type Synonym = (Column, ViewColumn)
type ViewColumn = 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 :: Field
, otDirection :: Maybe OrderDirection
, otNullOrder :: Maybe OrderNulls
} deriving (Show, Eq)
data QualifiedIdentifier = QualifiedIdentifier {
qiSchema :: Schema
, qiName :: TableName
} deriving (Show, Eq, Ord)
data RelationType = Child | Parent | Many | Root deriving (Show, Eq)
{-|
The name 'Relation' here is used with the meaning
"What is the relation between the current node and the parent node".
It has nothing to do with PostgreSQL referring to tables/views as relations.
The order of the relColumns and relFColumns should be maintained to get
the join conditions right.
TODO merge relColumns and relFColumns to a tuple or Data.Bimap
-}
data Relation = Relation {
relTable :: Table
, relColumns :: [Column]
, relFTable :: Table
, relFColumns :: [Column]
, relType :: RelationType
-- The Link attrs are used when RelationType == Many
, relLinkTable :: Maybe Table
, relLinkCols1 :: Maybe [Column]
, relLinkCols2 :: Maybe [Column]
} deriving (Show, Eq)
-- | Cached attributes of a JSON payload
data PayloadJSON = PayloadJSON {
-- | This is the raw ByteString that comes from the request body.
-- We cache this instead of an Aeson Value because it was detected that for large payloads the encoding
-- had high memory usage, see #1005 for more details
pjRaw :: BL.ByteString
, pjType :: PJType
-- | Keys of the object or if it's an array these keys are guaranteed to be the same across all its objects
, pjKeys :: S.Set Text
} deriving (Show, Eq)
data PJType = PJArray { pjaLength :: Int } | PJObject deriving (Show, Eq)
-- | e.g. whether it is []/{} or not
pjIsEmpty :: PayloadJSON -> Bool
pjIsEmpty (PayloadJSON _ PJObject keys) = S.size keys == 0
pjIsEmpty (PayloadJSON _ (PJArray l) _) = l == 0
data Proxy = Proxy {
proxyScheme :: Text
, proxyHost :: Text
, proxyPort :: Integer
, proxyPath :: Text
} deriving (Show, Eq)
type Operator = Text
operators :: M.HashMap Operator SqlFragment
operators = M.union (M.fromList [
("eq", "="),
("gte", ">="),
("gt", ">"),
("lte", "<="),
("lt", "<"),
("neq", "<>"),
("like", "LIKE"),
("ilike", "ILIKE"),
("in", "IN"),
("is", "IS"),
("cs", "@>"),
("cd", "<@"),
("ov", "&&"),
("sl", "<<"),
("sr", ">>"),
("nxr", "&<"),
("nxl", "&>"),
("adj", "-|-")]) ftsOperators
ftsOperators :: M.HashMap Operator SqlFragment
ftsOperators = M.fromList [
("fts", "@@ to_tsquery"),
("plfts", "@@ plainto_tsquery"),
("phfts", "@@ phraseto_tsquery")
]
data OpExpr = OpExpr Bool Operation deriving (Eq, Show)
data Operation = Op Operator SingleVal |
In ListVal |
Fts Operator (Maybe Language) SingleVal deriving (Eq, Show)
type Language = Text
-- | Represents a single value in a filter, e.g. id=eq.singleval
type SingleVal = Text
-- | Represents a list value in a filter, e.g. id=in.(val1,val2,val3)
type ListVal = [Text]
data LogicOperator = And | Or deriving Eq
instance Show LogicOperator where
show And = "AND"
show Or = "OR"
{-|
Boolean logic expression tree e.g. "and(name.eq.N,or(id.eq.1,id.eq.2))" is:
And
/ \
name.eq.N Or
/ \
id.eq.1 id.eq.2
-}
data LogicTree = Expr Bool LogicOperator [LogicTree] | Stmnt Filter deriving (Show, Eq)
type FieldName = Text
{-|
Json path operations as specified in https://www.postgresql.org/docs/9.4/static/functions-json.html
-}
type JsonPath = [JsonOperation]
-- | Represents the single arrow `->` or double arrow `->>` operators
data JsonOperation = JArrow{jOp :: JsonOperand} | J2Arrow{jOp :: JsonOperand} deriving (Show, Eq)
-- | Represents the key(`->'key'`) or index(`->'1`::int`), the index is Text because we reuse our escaping functons and let pg do the casting with '1'::int
data JsonOperand = JKey{jVal :: Text} | JIdx{jVal :: Text} deriving (Show, Eq)
type Field = (FieldName, JsonPath)
type Alias = Text
type Cast = Text
type NodeName = Text
-- Rpc query param, only used for GET rpcs
type RpcQParam = (Text, Text)
{-|
Custom guc header, it's obtained by parsing the json in a:
`SET LOCAL "response.headers" = '[{"Set-Cookie": ".."}]'
-}
newtype GucHeader = GucHeader (Text, Text)
instance JSON.FromJSON GucHeader where
parseJSON (JSON.Object o) = case headMay (M.toList o) of
Just (k, JSON.String s) | M.size o == 1 -> pure $ GucHeader (k, s)
| otherwise -> mzero
_ -> mzero
parseJSON _ = mzero
toHeaders :: [GucHeader] -> [Header]
toHeaders = map $ \(GucHeader (k, v)) -> (CI.mk $ toS k, toS v)
{-|
This type will hold information about which particular 'Relation' between two tables to choose when there are multiple ones.
Specifically, it will contain the name of the foreign key or the join table in many to many relations.
-}
type RelationDetail = Text
type SelectItem = (Field, Maybe Cast, Maybe Alias, Maybe RelationDetail)
-- | Path of the embedded levels, e.g "clients.projects.name=eq.." gives Path ["clients", "projects"]
type EmbedPath = [Text]
data Filter = Filter { field::Field, opExpr::OpExpr } deriving (Show, Eq)
data JoinCondition = JoinCondition (QualifiedIdentifier, Maybe Alias, FieldName)
(QualifiedIdentifier, Maybe Alias, FieldName) deriving (Show, Eq)
data ReadQuery = Select { select::[SelectItem], from::[TableName], where_::[LogicTree], joinConditions::[JoinCondition], order::[OrderTerm], range_::NonnegRange } deriving (Show, Eq)
data MutateQuery = Insert { in_::TableName, insPkCols::[Text], qPayload::PayloadJSON, onConflict:: Maybe PreferResolution, where_::[LogicTree], returning::[FieldName] }
| Delete { in_::TableName, where_::[LogicTree], returning::[FieldName] }
| Update { in_::TableName, qPayload::PayloadJSON, where_::[LogicTree], returning::[FieldName] } deriving (Show, Eq)
type ReadNode = (ReadQuery, (NodeName, Maybe Relation, Maybe Alias, Maybe RelationDetail, Depth))
type ReadRequest = Tree ReadNode
-- Depth of the ReadRequest tree
type Depth = Integer
type MutateRequest = MutateQuery
data DbRequest = DbRead ReadRequest | DbMutate MutateRequest
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
-- | Convert from ContentType to a full HTTP Header
toHeader :: ContentType -> Header
toHeader ct = (hContentType, toMime ct <> "; charset=utf-8")
-- | Convert from ContentType to a ByteString representing the mime type
toMime :: ContentType -> ByteString
toMime CTApplicationJSON = "application/json"
toMime CTTextCSV = "text/csv"
toMime CTOpenAPI = "application/openapi+json"
toMime CTSingularJSON = "application/vnd.pgrst.object+json"
toMime CTOctetStream = "application/octet-stream"
toMime CTAny = "*/*"
toMime (CTOther ct) = ct
data PgVersion = PgVersion {
pgvNum :: Int32
, pgvName :: Text
} deriving (Eq, Show)
instance Ord PgVersion where
(PgVersion v1 _) `compare` (PgVersion v2 _) = v1 `compare` v2
-- | Tells the minimum PostgreSQL version required by this version of PostgREST
minimumPgVersion :: PgVersion
minimumPgVersion = PgVersion 90400 "9.4"
pgVersion95 :: PgVersion
pgVersion95 = PgVersion 90500 "9.5"
pgVersion96 :: PgVersion
pgVersion96 = PgVersion 90600 "9.6"
pgVersion100 :: PgVersion
pgVersion100 = PgVersion 100000 "10"
sourceCTEName :: SqlFragment
sourceCTEName = "pg_source"
-- | full jspath, e.g. .property[0].attr.detail
type JSPath = [JSPathExp]
-- | jspath expression, e.g. .property, .property[0] or ."property-dash"
data JSPathExp = JSPKey Text | JSPIdx Int deriving (Eq, Show)
+17 -6
View File
@@ -1,7 +1,18 @@
flags: {}
packages:
- '.'
extra-deps:
resolver: lts-9.6
extra-deps:
- configurator-ng-0.0.0.1
- critbit-0.2.0.0
- hjsonpointer-1.1.1
- hjsonschema-1.5.0.1
- Ranged-sets-0.3.0
- packdeps-0.4.1
resolver: lts-3.1
- protolude-0.2.2
- hasql-1.3
- hasql-pool-0.5
- hasql-transaction-0.7
- text-builder-0.5.1.1
- jose-0.7.0.0
- postgresql-libpq-0.9.4.1
ghc-options:
postgrest: -O2 -Werror -Wall -fwarn-identities -fno-warn-redundant-constraints
nix:
packages: [postgresql, zlib]
+13
View File
@@ -0,0 +1,13 @@
FROM debian:jessie
ENV PATH /root/.local/bin:$PATH
RUN apt-get update \
&& apt-get install -y wget libpq-dev pkg-config libpcre3 libpcre3-dev \
postgresql-client debconf locales \
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/* \
&& echo 'en_US.UTF-8 UTF-8' > /etc/locale.gen \
&& locale-gen \
&& echo 'export LC_ALL=en_US.UTF-8' >> /etc/profile \
&& wget -qO- https://get.haskellstack.org/ | sh
+218
View File
@@ -0,0 +1,218 @@
module Feature.AndOrParamsSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai (Application)
import SpecHelper
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "and/or params used for complex boolean logic" $ do
context "used with GET" $ do
context "or param" $ do
it "can do simple logic" $
get "/entities?or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
it "can negate simple logic" $
get "/entities?not.or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
it "can be combined with traditional filters" $
get "/entities?or=(id.eq.1,id.eq.2)&name=eq.entity 1&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
context "embedded levels" $ do
it "can do logic on the second level" $
get "/entities?child_entities.or=(id.eq.1,name.eq.child entity 2)&select=id,child_entities(id)" `shouldRespondWith`
[json|[
{"id": 1, "child_entities": [ { "id": 1 }, { "id": 2 } ] }, { "id": 2, "child_entities": []},
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
]|] { matchHeaders = [matchContentTypeJson] }
it "can do logic on the third level" $
get "/entities?child_entities.grandchild_entities.or=(id.eq.1,id.eq.2)&select=id,child_entities(id,grandchild_entities(id))" `shouldRespondWith`
[json|[
{"id": 1, "child_entities": [ { "id": 1, "grandchild_entities": [ { "id": 1 }, { "id": 2 } ]}, { "id": 2, "grandchild_entities": []}]},
{"id": 2, "child_entities": [ { "id": 3, "grandchild_entities": []} ]},
{"id": 3, "child_entities": []}, {"id": 4, "child_entities": []}
]|] { matchHeaders = [matchContentTypeJson] }
context "and/or params combined" $ do
it "can be nested inside the same expression" $
get "/entities?or=(and(name.eq.entity 2,id.eq.2),and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
it "can be negated while nested" $
get "/entities?or=(not.and(name.eq.entity 2,id.eq.2),not.and(name.eq.entity 1,id.eq.1))&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
it "can be combined unnested" $
get "/entities?and=(id.eq.1,name.eq.entity 1)&or=(id.eq.1,id.eq.2)&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
context "operators inside and/or" $ do
it "can handle eq and neq" $
get "/entities?and=(id.eq.1,id.neq.2))&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
it "can handle lt and gt" $
get "/entities?or=(id.lt.2,id.gt.3)&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
it "can handle lte and gte" $
get "/entities?or=(id.lte.2,id.gte.3)&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
it "can handle like and ilike" $
get "/entities?or=(name.like.*1,name.ilike.*ENTITY 2)&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
it "can handle in" $
get "/entities?or=(id.in.(1,2),id.in.(3,4))&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
it "can handle is" $
get "/entities?and=(name.is.null,arr.is.null)&select=id" `shouldRespondWith`
[json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
it "can handle fts" $ do
get "/entities?or=(text_search_vector.fts.bar,text_search_vector.fts.baz)&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
get "/tsearch?or=(text_search_vector.plfts(german).Art%20Spass, text_search_vector.plfts(french).amusant%20impossible, text_search_vector.fts(english).impossible)" `shouldRespondWith`
[json|[
{"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
]|] { matchHeaders = [matchContentTypeJson] }
it "can handle cs and cd" $
get "/entities?or=(arr.cs.{1,2,3},arr.cd.{1})&select=id" `shouldRespondWith`
[json|[{ "id": 1 },{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
it "can handle range operators" $ do
get "/ranges?range=eq.[1,3]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=neq.[1,3]&select=id" `shouldRespondWith`
[json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=lt.[1,10]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=gt.[8,11]&select=id" `shouldRespondWith`
[json|[{ "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=lte.[1,3]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=gte.[2,3]&select=id" `shouldRespondWith`
[json|[{ "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=cs.[1,2]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=cd.[1,6]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=ov.[0,4]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=sl.[9,10]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=sr.[3,4]&select=id" `shouldRespondWith`
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=nxr.[4,7]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=nxl.[4,7]&select=id" `shouldRespondWith`
[json|[{ "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
get "/ranges?range=adj.(3,10]&select=id" `shouldRespondWith`
[json|[{ "id": 1 }]|] { matchHeaders = [matchContentTypeJson] }
context "operators with not" $ do
it "eq, cs, like can be negated" $
get "/entities?and=(arr.not.cs.{1,2,3},and(id.not.eq.2,name.not.like.*3))&select=id" `shouldRespondWith`
[json|[{ "id": 1}]|] { matchHeaders = [matchContentTypeJson] }
it "in, is, fts can be negated" $
get "/entities?and=(id.not.in.(1,3),and(name.not.is.null,text_search_vector.not.fts.foo))&select=id" `shouldRespondWith`
[json|[{ "id": 2}]|] { matchHeaders = [matchContentTypeJson] }
it "lt, gte, cd can be negated" $
get "/entities?and=(arr.not.cd.{1},or(id.not.lt.1,id.not.gte.3))&select=id" `shouldRespondWith`
[json|[{"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
it "gt, lte, ilike can be negated" $
get "/entities?and=(name.not.ilike.*ITY2,or(id.not.gt.4,id.not.lte.1))&select=id" `shouldRespondWith`
[json|[{"id": 1}, {"id": 2}, {"id": 3}]|] { matchHeaders = [matchContentTypeJson] }
context "and/or params with quotes" $ do
it "eq can have quotes" $
get "/grandchild_entities?or=(name.eq.\"(grandchild,entity,4)\",name.eq.\"(grandchild,entity,5)\")&select=id" `shouldRespondWith`
[json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
it "like and ilike can have quotes" $
get "/grandchild_entities?or=(name.like.\"*ity,4*\",name.ilike.\"*ITY,5)\")&select=id" `shouldRespondWith`
[json|[{ "id": 4 }, { "id": 5 }]|] { matchHeaders = [matchContentTypeJson] }
it "in can have quotes" $
get "/grandchild_entities?or=(id.in.(\"1\",\"2\"),id.in.(\"3\",\"4\"))&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }, { "id": 3 }, { "id": 4 }]|] { matchHeaders = [matchContentTypeJson] }
it "allows whitespace" $
get "/entities?and=( and ( id.in.( 1, 2, 3 ) , id.eq.3 ) , or ( id.eq.2 , id.eq.3 ) )&select=id" `shouldRespondWith`
[json|[{ "id": 3 }]|] { matchHeaders = [matchContentTypeJson] }
context "multiple and/or conditions" $ do
it "cannot have zero conditions" $
get "/entities?or=()" `shouldRespondWith`
[json|{
"details": "unexpected \")\" expecting field name (* or [a..z0..9_]), negation operator (not) or logic operator (and, or)",
"message": "\"failed to parse logic tree (())\" (line 1, column 4)"
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
it "can have a single condition" $ do
get "/entities?or=(id.eq.1)&select=id" `shouldRespondWith`
[json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
get "/entities?and=(id.eq.1)&select=id" `shouldRespondWith`
[json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
it "can have three conditions" $ do
get "/grandchild_entities?or=(id.eq.1, id.eq.2, id.eq.3)&select=id" `shouldRespondWith`
[json|[{"id":1}, {"id":2}, {"id":3}]|] { matchHeaders = [matchContentTypeJson] }
get "/grandchild_entities?and=(id.in.(1,2), id.in.(3,1), id.in.(1,4))&select=id" `shouldRespondWith`
[json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
it "can have four conditions combining and/or" $ do
get "/grandchild_entities?or=( id.eq.1, id.eq.2, and(id.in.(1,3), id.in.(2,3)), id.eq.4 )&select=id" `shouldRespondWith`
[json|[{"id":1}, {"id":2}, {"id":3}, {"id":4}]|] { matchHeaders = [matchContentTypeJson] }
get "/grandchild_entities?and=( id.eq.1, not.or(id.eq.2, id.eq.3), id.in.(1,4), or(id.eq.1, id.eq.4) )&select=id" `shouldRespondWith`
[json|[{"id":1}]|] { matchHeaders = [matchContentTypeJson] }
context "used with POST" $
it "includes related data with filters" $
request methodPost "/child_entities?select=id,entities(id)&entities.or=(id.eq.2,id.eq.3)&entities.order=id"
[("Prefer", "return=representation")]
[json|[{"id":4,"name":"entity 4","parent_id":1},
{"id":5,"name":"entity 5","parent_id":2},
{"id":6,"name":"entity 6","parent_id":3}]|] `shouldRespondWith`
[json|[{"id": 4, "entities":null}, {"id": 5, "entities": {"id": 2}}, {"id": 6, "entities": {"id": 3}}]|]
{ matchStatus = 201, matchHeaders = [matchContentTypeJson] }
context "used with PATCH" $
it "succeeds when using and/or params" $
request methodPatch "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
[("Prefer", "return=representation")]
[json|{ name : "updated grandchild entity"}|] `shouldRespondWith`
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
{ matchHeaders = [matchContentTypeJson] }
context "used with DELETE" $
it "succeeds when using and/or params" $
request methodDelete "/grandchild_entities?or=(id.eq.1,id.eq.2)&select=id,name"
[("Prefer", "return=representation")] "" `shouldRespondWith`
[json|[{ "id": 1, "name" : "updated grandchild entity"},{ "id": 2, "name" : "updated grandchild entity"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can query columns that begin with and/or reserved words" $
get "/grandchild_entities?or=(and_starting_col.eq.smth, or_starting_col.eq.smth)" `shouldRespondWith` 200
it "fails when using IN without () and provides meaningful error message" $
get "/entities?or=(id.in.1,2,id.eq.3)" `shouldRespondWith`
[json|{
"details": "unexpected \"1\" expecting \"(\"",
"message": "\"failed to parse logic tree ((id.in.1,2,id.eq.3))\" (line 1, column 10)"
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
it "fails on malformed query params and provides meaningful error message" $ do
get "/entities?or=)(" `shouldRespondWith`
[json|{
"details": "unexpected \")\" expecting \"(\"",
"message": "\"failed to parse logic tree ()()\" (line 1, column 3)"
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/entities?and=(ord(id.eq.1,id.eq.1),id.eq.2)" `shouldRespondWith`
[json|{
"details": "unexpected \"d\" expecting \"(\"",
"message": "\"failed to parse logic tree ((ord(id.eq.1,id.eq.1),id.eq.2))\" (line 1, column 7)"
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/entities?or=(id.eq.1,not.xor(id.eq.2,id.eq.3))" `shouldRespondWith`
[json|{
"details": "unexpected \"x\" expecting logic operator (and, or)",
"message": "\"failed to parse logic tree ((id.eq.1,not.xor(id.eq.2,id.eq.3)))\" (line 1, column 16)"
}|] { matchStatus = 400, matchHeaders = [matchContentTypeJson] }
+21
View File
@@ -0,0 +1,21 @@
module Feature.AsymmetricJwtSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude
-- }}}
spec :: SpecWith Application
spec = describe "server started with asymmetric JWK" $
-- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with jwt token signed with an asymmetric key" $ do
let auth = authHeaderJWT "eyJhbGciOiJSUzI1NiJ9.eyJyb2xlIjogInBvc3RncmVzdF90ZXN0X2F1dGhvciJ9Cg.CBOYWDvqgAR0YYnZnyDGTQi6AJLc2Pds6_eV3YuBG6I36mj_h05eLhkEKNEDA5ZteMzCiY83P60rC_xtxVd7B6vo3BeF5uoanPS3rrbuHzKPwzsrgrD_CqvEuJ4n7Q9epkQiLsNkcexneENZDRqFjbwZx3DrXiCWwlK3Ytr5NAIGxmy0od-0xNpb2U1nXQyO_Q3mumWFViRt4tmFn_3goDHNKG3Ha_AzImfUNvHnWL78kAc4rbn15vLtWXD8PwtSnZaB4lY4V6RfsaW937srQsmRetvytM1i_bHBnjkjQLAqGbXPyItjtlXPs0uGNBadE8-wgkLtfmSCC4v2DjUthw"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+47
View File
@@ -0,0 +1,47 @@
module Feature.AudienceJwtSecretSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
-- }}}
spec :: SpecWith Application
spec = describe "test handling of aud claims in JWT" $ do
-- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with jwt token containing with an audience claim" $ do
{- This is the decoded contents of authHeaderJWT
{
"exp": 9999999999,
"role": "postgrest_test_author",
"id": "jdoe",
"aud": "youraudience"
}
-}
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UiLCJhdWQiOiJ5b3VyYXVkaWVuY2UifQ.fJ4tLKSmolWGWehWN20qiU9dMO-WY0RI2VvacL7-ZGo"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "succeeds with jwt token that does not contain an audience claim" $ do
{- This is the decoded contents of authHeaderJWT
{
"exp": 9999999999,
"role": "postgrest_test_author",
"id": "jdoe"
}
-}
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "requests without JWT token should work" $
get "/has_count_column" `shouldRespondWith` 200
+132 -40
View File
@@ -1,61 +1,153 @@
module Feature.AuthSpec where
-- {{{ Imports
import Text.Heredoc
import Test.Hspec
import Test.Hspec.Wai
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
import Protolude hiding (get)
it "hides tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` 404
spec :: SpecWith Application
spec = describe "authorization" $ do
let single = ("Accept","application/vnd.pgrst.object+json")
it "indicates login failure (BasicAuth)" $ do
let auth = authHeaderBasic "postgrest_test_author" "fakefake"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "denies access to tables that anonymous does not own" $
get "/authors_only" `shouldRespondWith` [json| {
"hint":null,
"details":null,
"code":"42501",
"message":"permission denied for relation authors_only"} |]
{ matchStatus = 401
, matchHeaders = ["WWW-Authenticate" <:> "Bearer"]
}
it "allows users with permissions to see their tables (BasicAuth)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
it "denies access to tables that postgrest_test_author does not own" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in
request methodGet "/private_table" [auth] ""
`shouldRespondWith` [json| {
"hint":null,
"details":null,
"code":"42501",
"message":"permission denied for relation private_table"} |]
{ matchStatus = 403
, matchHeaders = []
}
it "denies execution on functions that anonymous does not own" $
post "/rpc/privileged_hello" [json|{"name": "anonymous"}|] `shouldRespondWith` 401
it "allows execution on a function that postgrest_test_author owns" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA" in
request methodPost "/rpc/privileged_hello" [auth] [json|{"name": "jdoe"}|]
`shouldRespondWith` [json|"Privileged hello to jdoe"|]
{ matchStatus = 200
, matchHeaders = [matchContentTypeJson]
}
it "returns jwt functions as jwt tokens" $
request methodPost "/rpc/login" [single]
[json| { "id": "jdoe", "pass": "1234" } |]
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xuYW1lIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.KO-0PGp_rU-utcDBP6qwdd-Th2Fk-ICVt01I7QtTDWs"} |]
{ matchStatus = 200
, matchHeaders = [matchContentTypeSingular]
}
it "sql functions can encode custom and standard claims" $
request methodPost "/rpc/jwt_test" [single] "{}"
`shouldRespondWith` [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJqb2UiLCJzdWIiOiJmdW4iLCJhdWQiOiJldmVyeW9uZSIsImV4cCI6MTMwMDgxOTM4MCwibmJmIjoxMzAwODE5MzgwLCJpYXQiOjEzMDA4MTkzODAsImp0aSI6ImZvbyIsInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdCIsImh0dHA6Ly9wb3N0Z3Jlc3QuY29tL2ZvbyI6dHJ1ZX0.G2REtPnOQMUrVRDA9OnkPJTd8R0tf4wdYOlauh1E2Ek"} |]
{ matchStatus = 200
, matchHeaders = [matchContentTypeSingular]
}
it "sql functions can read custom and standard claims variables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiJmdW4iLCJqdGkiOiJmb28iLCJuYmYiOjEzMDA4MTkzODAsImV4cCI6OTk5OTk5OTk5OSwiaHR0cDovL3Bvc3RncmVzdC5jb20vZm9vIjp0cnVlLCJpc3MiOiJqb2UiLCJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWF0IjoxMzAwODE5MzgwfQ.V5fEpXfpb7feqwVqlcDleFdKu86bdwU2cBRT4fcMhXg"
request methodPost "/rpc/reveal_big_jwt" [auth] "{}"
`shouldRespondWith` [str|[{"iss":"joe","sub":"fun","exp":9999999999,"nbf":1300819380,"iat":1300819380,"jti":"foo","http://postgrest.com/foo":true}]|]
it "allows users with permissions to see their tables" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "recovers after 400 error with logged in user" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
it "works with tokens which have extra fields" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIiwia2V5MSI6InZhbHVlMSIsImtleTIiOiJ2YWx1ZTIiLCJrZXkzIjoidmFsdWUzIiwiYSI6MSwiYiI6MiwiYyI6M30.b0eglDKYEmGi-hCvD-ddSqFl7vnDO5qkUaviaHXm3es"
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.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "fails with an expired token" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE0NDY2NzgxNDksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.f8__E6VQwYcDqwHmr9PG03uaZn8Zh1b0vbJ9DYS0AdM"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` [json| {"message":"JWT expired"} |]
{ matchStatus = 401
, matchHeaders = [
"WWW-Authenticate" <:>
"Bearer error=\"invalid_token\", error_description=\"JWT expired\""
]
}
it "hides tables from users with invalid JWT" $ do
let auth = authHeaderJWT "ey9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` [json| {"message":"JWSError (CompactDecodeError \"expected 3 parts, got 2\")"} |]
{ matchStatus = 401
, matchHeaders = [
"WWW-Authenticate" <:>
"Bearer error=\"invalid_token\", error_description=\"JWSError (CompactDecodeError \\\"expected 3 parts, got 2\\\")\""
]
}
it "should fail when jwt contains no claims" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.e30.CUIP5V9thWsGGFsFyGijSZf1fJMfarLHI9CEJL-TGNk"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "hides tables from users with JWT that contain no claims about role" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Impkb2UifQ.RVlZDaSyKbFPvxUf3V_NQXybfRB4dlBIkAUQXVXLUAI"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 401
it "recovers after 401 error with logged in user" $ do
_ <- post "/authors_only" [json| { "owner": "jdoe", "secret": "test content" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
_ <- request methodPost "/rpc/problem" [auth] ""
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "allows users to login (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "1234" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"} |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json"]
}
describe "custom pre-request proc acting on id claim" $ do
it "indicates login failure (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
post "/postgrest/tokens" [json| { "id":"jdoe", "pass": "NOPE" } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| {"message":"Failed authentication."} |]
, matchStatus = 401
, matchHeaders = ["Content-Type" <:> "application/json"]
}
it "able to switch to postgrest_test_author role (id=1)" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6MX0.gKw7qI50i9hMrSJW8BlTpdMEVmMXJYxlAqueGqpa_mE" in
request methodPost "/rpc/get_current_user" [auth]
[json| {} |]
`shouldRespondWith` [str|"postgrest_test_author"|]
{ matchStatus = 200
, matchHeaders = []
}
it "allows users with permissions to see their tables (JWT)" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqZG9lIn0.y4vZuu1dDdwAl0-S00MCRWRYMlJ5YAMSir6Es6WtWx0"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
it "able to switch to postgrest_test_default_role (id=2)" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6Mn0.nwzjMI0YLvVGJQTeoCPEBsK983b__gxdpLXisBNaO2A" in
request methodPost "/rpc/get_current_user" [auth]
[json| {} |]
`shouldRespondWith` [str|"postgrest_test_default_role"|]
{ matchStatus = 200
, matchHeaders = []
}
it "raises error (id=3)" $
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpZCI6M30.OGxEJAf60NKZiTn-tIb2jy4rqKs_ZruLGWZ40TjrJsM" in
request methodPost "/rpc/get_current_user" [auth]
[json| {} |]
`shouldRespondWith` [str|{"hint":"Please contact administrator","details":null,"code":"P0001","message":"Disabled ID --> 3"}|]
{ matchStatus = 400
, matchHeaders = []
}
+21
View File
@@ -0,0 +1,21 @@
module Feature.BinaryJwtSecretSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude
-- }}}
spec :: SpecWith Application
spec = describe "server started with binary JWT secret" $
-- this test will stop working 9999999999s after the UNIX EPOCH
it "succeeds with jwt token encoded with a binary secret" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 200
+52
View File
@@ -0,0 +1,52 @@
{-# 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
import Test.Hspec.Wai.Internal
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai.Test (Session)
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "Queryiny in parallel" $
it "should not raise 'transaction in progress' error" $
raceTest 10 $
get "/fakefake"
`shouldRespondWith` [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
+8 -4
View File
@@ -9,10 +9,14 @@ import qualified Data.ByteString.Lazy as BL
import SpecHelper
import Network.HTTP.Types
import Network.Wai (Application)
import Protolude
-- }}}
spec :: Spec
spec = around withApp $ describe "CORS" $ do
spec :: SpecWith Application
spec =
describe "CORS" $ do
let preflightHeaders = [
("Accept", "*/*"),
("Origin", "http://example.com"),
@@ -41,7 +45,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, PUT, DELETE, OPTIONS, HEAD"
respHeaders `shouldSatisfy` matchHeader
"Access-Control-Allow-Headers"
"Authentication, Foo, Bar, Accept, Accept-Language, Content-Language"
@@ -67,4 +71,4 @@ spec = around withApp $ describe "CORS" $ do
liftIO $ do
simpleHeaders r `shouldSatisfy` matchHeader
"Access-Control-Allow-Origin" "\\*"
simpleBody r `shouldSatisfy` not . BL.null
simpleBody r `shouldSatisfy` BL.null
+41 -14
View File
@@ -2,35 +2,62 @@ 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 $
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "Deleting" $ do
context "existing record" $ do
it "succeeds with 204 and deletion count" $
request methodDelete "/items?id=eq.1" [] ""
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 204
`shouldRespondWith` ""
{ matchStatus = 204
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns the deleted item and count if requested" $
request methodDelete "/items?id=eq.2" [("Prefer", "return=representation"), ("Prefer", "count=exact")] ""
`shouldRespondWith` [str|[{"id":2}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/1"]
}
it "returns the deleted item and shapes the response" $
request methodDelete "/complex_items?id=eq.2&select=id,name" [("Prefer", "return=representation")] ""
`shouldRespondWith` [str|[{"id":2,"name":"Two"}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "can rename and cast the selected columns" $
request methodDelete "/complex_items?id=eq.3&select=ciId:id::text,ciName:name" [("Prefer", "return=representation")] ""
`shouldRespondWith` [str|[{"ciId":"3","ciName":"Three"}]|]
it "can embed (parent) entities" $
request methodDelete "/tasks?id=eq.8&select=id,name,project(id)" [("Prefer", "return=representation")] ""
`shouldRespondWith` [str|[{"id":8,"name":"Code OSX","project":{"id":4}}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "actually clears items ouf the db" $ do
_ <- request methodDelete "/items?id=lt.15" [] ""
get "/items"
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[{\"id\":15}]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/1"]
`shouldRespondWith` [str|[{"id":15}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
context "known route, unknown record" $
it "fails with 404" $
request methodDelete "/items?id=eq.101" [] "" `shouldRespondWith` 404
context "known route, no records matched" $
it "includes [] body if return=rep" $
request methodDelete "/items?id=eq.101"
[("Prefer", "return=representation")] ""
`shouldRespondWith` "[]"
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
context "totally unknown route" $
it "fails with 404" $
+37
View File
@@ -0,0 +1,37 @@
module Feature.ExtraSearchPathSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude
spec :: SpecWith Application
spec = describe "extra search path" $ do
it "finds the ltree <@ operator on the public schema" $
request methodGet "/ltree_sample?path=cd.Top.Science.Astronomy" [] ""
`shouldRespondWith` [json|[
{"path":"Top.Science.Astronomy"},
{"path":"Top.Science.Astronomy.Astrophysics"},
{"path":"Top.Science.Astronomy.Cosmology"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "finds the ltree nlevel function on the public schema, used through a computed column" $
request methodGet "/ltree_sample?select=number_of_labels&path=eq.Top.Science" [] ""
`shouldRespondWith` [json|[{"number_of_labels":2}]|]
{ matchHeaders = [matchContentTypeJson] }
it "finds the isn = operator on the extensions schema" $
request methodGet "/isn_sample?id=eq.978-0-393-04002-9&select=name" [] ""
`shouldRespondWith` [json|[{"name":"Mathematics: From the Birth of Numbers"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "finds the isn is_valid function on the extensions schema" $
request methodGet "/rpc/is_valid_isbn?input=978-0-393-04002-9" [] ""
`shouldRespondWith` [json|true|]
{ matchHeaders = [matchContentTypeJson] }
+333 -155
View File
@@ -1,37 +1,94 @@
module Feature.InsertSpec where
import Test.Hspec
import Test.Hspec hiding (pendingWith)
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Test.Hspec.Wai.Matcher (bodyEquals)
import Network.Wai.Test (SResponse(simpleBody,simpleHeaders,simpleStatus))
import SpecHelper
import qualified Data.Aeson as JSON
import Data.List (lookup)
import Data.Maybe (fromJust)
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
import Protolude hiding (get)
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
-- should not have content type set when body is empty
lookup hContentType (simpleHeaders p) `shouldBe` Nothing
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` [str|[{"integer":14,"varchar":"testing!"}]|]
{ matchStatus = 201
, matchHeaders = [matchContentTypeJson]
}
context "non uniform json array" $ do
it "rejects json array that isn't exclusivily composed of objects" $
post "/articles" [json| [{"id": 100, "body": "xxxxx"}, 123, "xxxx", {"id": 111, "body": "xxxx"}] |] `shouldRespondWith` 400
it "rejects json array that has objects with different keys" $
post "/articles" [json| [{"id": 100, "body": "xxxxx"}, {"id": 111, "body": "xxxx", "owner": "me"}] |] `shouldRespondWith` 400
context "requesting full representation" $ do
it "includes related data after insert" $
request methodPost "/projects?select=id,name,clients(id,name)"
[("Prefer", "return=representation"), ("Prefer", "count=exact")]
[str|{"id":6,"name":"New Project","client_id":2}|] `shouldRespondWith` [str|[{"id":6,"name":"New Project","clients":{"id":2,"name":"Apple"}}]|]
{ matchStatus = 201
, matchHeaders = [ matchContentTypeJson
, "Location" <:> "/projects?id=eq.6"
, "Content-Range" <:> "*/1" ]
}
it "can rename and cast the selected columns" $
request methodPost "/projects?select=pId:id::text,pName:name,cId:client_id::text"
[("Prefer", "return=representation")]
[str|{"id":7,"name":"New Project","client_id":2}|] `shouldRespondWith`
[str|[{"pId":"7","pName":"New Project","cId":"2"}]|]
{ matchStatus = 201
, matchHeaders = [ matchContentTypeJson
, "Location" <:> "/projects?id=eq.7"
, "Content-Range" <:> "*/*" ]
}
context "from an html form" $
it "accepts disparate json types" $ do
p <- request methodPost "/menagerie"
[("Content-Type", "application/x-www-form-urlencoded")]
("integer=7&double=2.71828&varchar=forms+are+fun&" <>
"boolean=false&date=1900-01-01&money=$3.99&enum=foo")
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` created201
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
@@ -46,11 +103,13 @@ spec = afterAll_ resetDb $ around withApp $ do
incNullableStr record `shouldBe` Nothing
context "into a table with simple pk" $
it "fails with 400 and error" $
post "/simple_pk" [json| { "extra":"foo"} |]
`shouldRespondWith` 400
it "fails with 400 and error" $ do
p <- post "/simple_pk" [json| { "extra":"foo"} |]
liftIO $ do
simpleStatus p `shouldBe` badRequest400
isErrorFormat (simpleBody p) `shouldBe` True
context "into a table with no pk" . after_ (clearTable "no_pk") $ do
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
@@ -63,164 +122,205 @@ spec = afterAll_ resetDb $ around withApp $ do
[("Prefer", "return=representation")]
[json| { "a":"bar", "b":"baz" } |]
liftIO $ do
simpleBody p `shouldBe` [json| { "a":"bar", "b":"baz" } |]
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 "returns empty array when no items inserted, and return=rep" $ do
p <- request methodPost "/no_pk"
[("Prefer", "return=representation")]
[json| [] |]
liftIO $ do
simpleBody p `shouldBe` [json| [] |]
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" } |]
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" . after_ (clearTable "compound_pk") $
it "builds response location header appropriately" $
post "/compound_pk" [json| { "k1":12, "k2":42 } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 201,
matchHeaders = ["Location" <:> "/compound_pk?k1=eq.12&k2=eq.42"]
}
context "with compound pk supplied" $
it "builds response location header appropriately" $ do
let inserted = [json| { "k1":12, "k2":"Rock & R+ll" } |]
expectedObj = CompoundPK 12 "Rock & R+ll" Nothing
expectedLoc = "/compound_pk?k1=eq.12&k2=eq.Rock%20%26%20R%2Bll"
p <- request methodPost "/compound_pk"
[("Prefer", "return=representation")]
inserted
liftIO $ do
JSON.decode (simpleBody p) `shouldBe` Just [expectedObj]
simpleStatus p `shouldBe` created201
lookup hLocation (simpleHeaders p) `shouldBe` Just expectedLoc
r <- get expectedLoc
liftIO $ do
JSON.decode (simpleBody r) `shouldBe` Just [expectedObj]
simpleStatus r `shouldBe` ok200
context "with bulk insert" $
it "returns 201 but no location header" $ do
let bulkData = [json| [ {"k1":21, "k2":"hello world"}
, {"k1":22, "k2":"bye for now"}]
|]
p <- request methodPost "/compound_pk" [] bulkData
liftIO $ do
simpleStatus p `shouldBe` created201
lookup hLocation (simpleHeaders p) `shouldBe` Nothing
context "with invalid json payload" $
it "fails with 400 and error" $
post "/simple_pk" "}{ x = 2" `shouldRespondWith` 400
it "fails with 400 and error" $ do
p <- post "/simple_pk" "}{ x = 2"
liftIO $ do
simpleStatus p `shouldBe` badRequest400
isErrorFormat (simpleBody p) `shouldBe` True
context "jsonb" . after_ (clearTable "json") $ do
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" } } |]
p <- request methodPost "json" [("Prefer", "return=representation")] inserted
liftIO $ do
simpleBody p `shouldBe` inserted
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%7B%22foo%22%3A%22bar%22%7D"
simpleStatus p `shouldBe` created201
location = "/json?data=eq.%7B%22foo%22%3A%22bar%22%7D"
request methodPost "/json"
[("Prefer", "return=representation")]
inserted
`shouldRespondWith` [str|[{"data":{"foo":"bar"}}]|]
{ matchStatus = 201
, matchHeaders = ["Location" <:> location]
}
it "serializes nested array" $ do
let inserted = [json| { "data": [1,2,3] } |]
p <- request methodPost "json" [("Prefer", "return=representation")] inserted
liftIO $ do
simpleBody p `shouldBe` inserted
simpleHeaders p `shouldSatisfy` matchHeader hLocation "/json\\?data=eq\\.%5B1%2C2%2C3%5D"
simpleStatus p `shouldBe` created201
location = "/json?data=eq.%5B1%2C2%2C3%5D"
request methodPost "/json"
[("Prefer", "return=representation")]
inserted
`shouldRespondWith` [str|[{"data":[1,2,3]}]|]
{ matchStatus = 201
, matchHeaders = ["Location" <:> location]
}
context "empty object" $
it "successfully populates table with all-default columns" $
post "/items" "{}" `shouldRespondWith` ""
{ matchStatus = 201
, matchHeaders = []
}
context "table with limited privileges" $ do
it "succeeds if correct select is applied" $
request methodPost "/limited_article_stars?select=article_id,user_id" [("Prefer", "return=representation")]
[json| {"article_id": 2, "user_id": 1} |] `shouldRespondWith` [str|[{"article_id":2,"user_id":1}]|]
{ matchStatus = 201
, matchHeaders = []
}
it "fails if more columns are selected" $
request methodPost "/limited_article_stars?select=article_id,user_id,created_at" [("Prefer", "return=representation")]
[json| {"article_id": 2, "user_id": 2} |] `shouldRespondWith`
[str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
{ matchStatus = 401
, matchHeaders = []
}
it "fails if select is not specified" $
request methodPost "/limited_article_stars" [("Prefer", "return=representation")]
[json| {"article_id": 3, "user_id": 1} |] `shouldRespondWith` [str|{"hint":null,"details":null,"code":"42501","message":"permission denied for relation limited_article_stars"}|]
{ matchStatus = 401
, matchHeaders = []
}
describe "CSV insert" $ do
after_ (clearTable "menagerie") . context "disparate csv types" $
context "disparate csv types" $
it "succeeds with multipart response" $ do
p <- request methodPost "/menagerie" [("Content-Type", "text/csv")]
[str|integer,double,varchar,boolean,date,money,enum
|13,3.14159,testing!,false,1900-01-01,$3.99,foo
|12,0.1,a string,true,1929-10-01,12,bar
|]
liftIO $ do
simpleBody p `shouldBe` "Content-Type: application/json\nLocation: /menagerie?integer=eq.13\n\n\n--postgrest_boundary\nContent-Type: application/json\nLocation: /menagerie?integer=eq.12\n\n"
simpleStatus p `shouldBe` created201
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
{ matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
, matchBody = bodyEquals inserted
}
after_ (clearTable "no_pk") . context "requesting full representation" $ do
context "requesting full representation" $ do
it "returns full details of inserted record" $
request methodPost "/no_pk"
[("Content-Type", "text/csv"), ("Prefer", "return=representation")]
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nbar,baz"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| { "a":"bar", "b":"baz" } |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json",
`shouldRespondWith` "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"), ("Prefer", "return=representation")]
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"a,b\nNULL,foo"
`shouldRespondWith` ResponseMatcher {
matchBody = Just [json| { "a":null, "b":"foo" } |]
, matchStatus = 201
, matchHeaders = ["Content-Type" <:> "application/json",
`shouldRespondWith` "a,b\n,foo"
{ matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Location" <:> "/no_pk?a=is.null&b=eq.foo"]
}
after_ (clearTable "no_pk") . context "with wrong number of columns" $ do
it "only returns the requested column header with its associated data" $
request methodPost "/projects?select=id"
[("Content-Type", "text/csv"), ("Accept", "text/csv"), ("Prefer", "return=representation")]
"id,name,client_id\n8,Xenix,1\n9,Windows NT,1"
`shouldRespondWith` "id\n8\n9"
{ matchStatus = 201
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8",
"Content-Range" <:> "*/*"]
}
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
it "fails for too many" $ do
p <- request methodPost "/no_pk" [("Content-Type", "text/csv")] "a,b\nfoo,bar\nbaz,bat,bad"
liftIO $ simpleStatus p `shouldBe` badRequest400
liftIO $ do
simpleStatus p `shouldBe` badRequest400
isErrorFormat (simpleBody p) `shouldBe` True
describe "Putting record" $ do
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
context "to unkonwn uri" $
it "gives a 404" $
request methodPut "/fake" []
[json| { "real": false } |]
`shouldRespondWith` 404
let Just location = lookup hLocation $ simpleHeaders p
r <- get location
liftIO $ simpleBody r `shouldBe` "["<>payload<>"]"
context "to a known uri" $ do
context "without a fully-specified primary key" $
it "is not an allowed operation" $
request methodPut "/compound_pk?k1=eq.12" []
[json| { "k1":12, "k2":42 } |]
`shouldRespondWith` 405
context "with a fully-specified primary key" $ do
context "not specifying every column in the table" $
it "is rejected for lack of idempotence" $
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
it "can create a new record" $ do
p <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":3 } |]
liftIO $ do
simpleBody p `shouldBe` ""
simpleStatus p `shouldBe` status204
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundK1 record `shouldBe` 12
compoundK2 record `shouldBe` 42
compoundExtra record `shouldBe` Just 3
it "can update an existing record" $ do
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":4 } |]
_ <- request methodPut "/compound_pk?k1=eq.12&k2=eq.42" []
[json| { "k1":12, "k2":42, "extra":5 } |]
r <- get "/compound_pk?k1=eq.12&k2=eq.42"
let rows = fromJust (JSON.decode $ simpleBody r :: Maybe [CompoundPK])
liftIO $ do
length rows `shouldBe` 1
let record = head rows
compoundExtra record `shouldBe` Just 5
context "with an auto-incrementing primary key" . after_ (clearTable "auto_incrementing_pk") $
it "succeeds with 204" $
request methodPut "/auto_incrementing_pk?id=eq.1" []
[json| {
"id":1,
"nullable_string":"hi",
"non_nullable_string":"bye",
"inserted_at": "2020-11-11"
} |]
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 204,
matchHeaders = []
}
describe "Patching record" $ do
context "to unkonwn uri" $
context "to unknown uri" $
it "gives a 404" $
request methodPatch "/fake" []
[json| { "real": false } |]
@@ -228,26 +328,48 @@ spec = afterAll_ resetDb $ around withApp $ do
context "on an empty table" $
it "indicates no records found to update" $
request methodPatch "/simple_pk" []
request methodPatch "/empty_table" []
[json| { "extra":20 } |]
`shouldRespondWith` 404
`shouldRespondWith` ""
{ matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
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" []
[json| { "id":42 } |]
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing,
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "0-0/1"]
}
`shouldSatisfy` matchHeader "Content-Range" "\\*/\\*"
p <- request methodPatch "/items?id=eq.2" [] [json| { "id":42 } |]
pure p `shouldRespondWith` ""
{ matchStatus = 204,
matchHeaders = ["Content-Range" <:> "0-0/*"]
}
liftIO $ lookup hContentType (simpleHeaders p) `shouldBe` Nothing
-- check it really got updated
g' <- get "/items?id=eq.42"
liftIO $ simpleHeaders g'
`shouldSatisfy` matchHeader "Content-Range" "0-0/1"
`shouldSatisfy` matchHeader "Content-Range" "0-0/\\*"
-- put value back for other tests
void $ request methodPatch "/items?id=eq.42" [] [json| { "id":2 } |]
it "returns empty array when no rows updated and return=rep" $
request methodPatch "/items?id=eq.999999"
[("Prefer", "return=representation")] [json| { "id":999999 } |]
`shouldRespondWith` "[]"
{
matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns updated object as array when return=rep" $
request methodPatch "/items?id=eq.2"
[("Prefer", "return=representation")] [json| { "id":2 } |]
`shouldRespondWith` [str|[{"id":2}]|]
{ matchStatus = 200,
matchHeaders = ["Content-Range" <:> "0-0/*"]
}
it "can update multiple items" $ do
replicateM_ 10 $ post "/auto_incrementing_pk"
@@ -259,14 +381,25 @@ spec = afterAll_ resetDb $ around withApp $ do
[json| { non_nullable_string: "c" } |]
g <- get "/auto_incrementing_pk?non_nullable_string=eq.c"
liftIO $ simpleHeaders g
`shouldSatisfy` matchHeader "Content-Range" "0-9/10"
`shouldSatisfy` matchHeader "Content-Range" "0-9/\\*"
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 }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can update based on a computed column" $
request methodPatch
"/items?always_true=eq.false"
[("Prefer", "return=representation")]
[json| { id: 100 } |]
`shouldRespondWith` 404
`shouldRespondWith` "[]"
{ matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"]
}
it "can provide a representation" $ do
_ <- post "/items"
[json| { id: 1 } |]
@@ -275,23 +408,68 @@ spec = afterAll_ resetDb $ around withApp $ do
[("Prefer", "return=representation")]
[json| { id: 99 } |]
`shouldRespondWith` [json| [{id:99}] |]
{ matchHeaders = [matchContentTypeJson] }
-- put value back for other tests
void $ request methodPatch "/items?id=eq.99" [] [json| { "id":1 } |]
it "makes no updates and returns 204, when patching with an empty json object/array" $ do
request methodPatch "/items" [] [json| {} |]
`shouldRespondWith` ""
{
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
request methodPatch "/items" [] [json| [] |]
`shouldRespondWith` ""
{
matchStatus = 204,
matchHeaders = ["Content-Range" <:> "*/*"]
}
get "/items" `shouldRespondWith`
[json|[{"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},{id:16},{"id":2},{"id":1}]|]
{ matchHeaders = [matchContentTypeJson] }
it "makes no updates and and returns 200, when patching with an empty json object and return=rep" $ do
request methodPatch "/items" [("Prefer", "return=representation")] [json| {} |]
`shouldRespondWith` "[]"
{
matchStatus = 200,
matchHeaders = ["Content-Range" <:> "*/*"]
}
get "/items" `shouldRespondWith`
[json| [{"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},{id:16},{"id":2},{"id":1}] |]
{ matchHeaders = [matchContentTypeJson] }
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.B-lReuGNDwAlU1GOC476MlO0vAt9JNoHIlxg2vwMaO0"
_ <- 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"
[ authHeaderBasic "jdoe" "1234", ("Prefer", "return=representation") ]
[ auth, ("Prefer", "return=representation") ]
[json| { "secret": "nyancat" } |]
liftIO $ do
simpleBody p1 `shouldBe` [json| { "owner":"jdoe", "secret":"nyancat" } |]
simpleStatus p1 `shouldBe` created201
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") ]
[ authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIiwiaWQiOiJqcm9lIn0.2e7mx0U4uDcInlbJVOBGlrRufwqWLINDIEDC1vS0nw8", ("Prefer", "return=representation") ]
[json| { "secret": "lolcat", "owner": "hacker" } |]
liftIO $ do
simpleBody p2 `shouldBe` [json| { "owner":"jroe", "secret":"lolcat" } |]
simpleStatus p2 `shouldBe` created201
simpleBody p2 `shouldBe` [str|[{"owner":"jroe","secret":"lolcat"}]|]
simpleStatus p2 `shouldBe` created201
+189
View File
@@ -0,0 +1,189 @@
module Feature.JsonOperatorSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec = describe "json and jsonb operators" $ do
context "Shaping response with select parameter" $ do
it "obtains a json subfield one level with casting" $
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"
{ matchHeaders = [matchContentTypeJson] }
it "renames json subfield one level with casting" $
get "/complex_items?id=eq.1&select=myFoo:settings->>foo::json" `shouldRespondWith`
[json| [{"myFoo":{"int":1,"bar":"baz"}}] |] -- the value of foo here is of type "text"
{ matchHeaders = [matchContentTypeJson] }
it "fails on bad casting (data of the wrong format)" $
get "/complex_items?select=settings->foo->>bar::integer"
`shouldRespondWith` [json| {"hint":null,"details":null,"code":"22P02","message":"invalid input syntax for integer: \"baz\""} |]
{ matchStatus = 400 , matchHeaders = [] }
it "obtains a json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=settings->foo->>bar" `shouldRespondWith`
[json| [{"bar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "renames json subfield two levels (string)" $
get "/complex_items?id=eq.1&select=myBar:settings->foo->>bar" `shouldRespondWith`
[json| [{"myBar":"baz"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "obtains a 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
{ matchHeaders = [matchContentTypeJson] }
it "renames json subfield two levels with casting (int)" $
get "/complex_items?id=eq.1&select=myInt:settings->foo->>int::integer" `shouldRespondWith`
[json| [{"myInt":1}] |] -- the value in the db is an int, but here we expect a string for now
{ matchHeaders = [matchContentTypeJson] }
-- TODO the status code for the error is 404, this is because 42883 represents undefined function
-- this works fine for /rpc/unexistent requests, but for this case a 500 seems more appropriate
it "fails when a double arrow ->> is followed with a single arrow ->" $ do
get "/json_arr?select=data->>c->1"
`shouldRespondWith` [json|
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
"details":null,"code":"42883","message":"operator does not exist: text -> integer"} |]
{ matchStatus = 404 , matchHeaders = [] }
get "/json_arr?select=data->>c->b"
`shouldRespondWith` [json|
{"hint":"No operator matches the given name and argument type(s). You might need to add explicit type casts.",
"details":null,"code":"42883","message":"operator does not exist: text -> unknown"} |]
{ matchStatus = 404 , matchHeaders = [] }
context "with array index" $ do
it "can get array of ints and alias/cast it" $ do
get "/json_arr?select=data->>0::int&id=in.(1,2)" `shouldRespondWith`
[json| [{"data":1}, {"data":4}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=idx0:data->>0::int,idx1:data->>1::int&id=in.(1,2)" `shouldRespondWith`
[json| [{"idx0":1,"idx1":2}, {"idx0":4,"idx1":5}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can get nested array of ints" $ do
get "/json_arr?select=data->0->>1::int&id=in.(3,4)" `shouldRespondWith`
[json| [{"data":8}, {"data":7}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->0->>1::int&id=in.(3,4)" `shouldRespondWith`
[json| [{"data":null}, {"data":6}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can get array of objects" $ do
get "/json_arr?select=data->0->>a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->a->>2&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":null}, {"a":"3"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can get array in object keys" $ do
get "/json_arr?select=data->c->>0::json&id=in.(7,8)" `shouldRespondWith`
[json| [{"c":1}, {"c":{"d": [4,5,6,7,8]}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->c->0->d->>4::int&id=in.(7,8)" `shouldRespondWith`
[json| [{"d":null}, {"d":8}] |]
{ matchHeaders = [matchContentTypeJson] }
it "only treats well formed numbers as indexes" $
get "/json_arr?select=data->0->0xy1->1->23-xy-45->1->xy-6->>0::int&id=eq.9" `shouldRespondWith`
[json| [{"xy-6":3}] |]
{ matchHeaders = [matchContentTypeJson] }
context "finishing json path with single arrow ->" $ do
it "works when finishing with a key" $ do
get "/json_arr?select=data->c&id=in.(7,8)" `shouldRespondWith`
[json| [{"c":[1,2,3]}, {"c":[{"d": [4,5,6,7,8]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "works when finishing with an index" $ do
get "/json_arr?select=data->0->a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":[1,2,3]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->c->0->d&id=eq.8" `shouldRespondWith`
[json| [{"d":[4,5,6,7,8]}] |]
{ matchHeaders = [matchContentTypeJson] }
context "filtering response" $ 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"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json?data->foo->>bar=eq.fake" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using not" $
get "/json?data->foo->>bar=not.eq.baz" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter by properties inside json column using ->>" $
get "/json?data->>id=eq.1" `shouldRespondWith`
[json| [{"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can be filtered with and/or" $
get "/grandchild_entities?or=(jsonb_col->a->>b.eq.foo, jsonb_col->>b.eq.bar)&select=id" `shouldRespondWith`
[json|[{id: 4}, {id: 5}]|] { matchStatus = 200, matchHeaders = [matchContentTypeJson] }
it "can filter by array indexes" $ do
get "/json_arr?select=data&data->>0=eq.1" `shouldRespondWith`
[json| [{"data":[1, 2, 3]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->1->>2=eq.13" `shouldRespondWith`
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->1->>b=eq.B" `shouldRespondWith`
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->1->b->>1=eq.5" `shouldRespondWith`
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter jsonb" $ do
get "/jsonb_test?data=eq.{\"e\":1}" `shouldRespondWith`
[json| [{"id":4,"data":{"e": 1}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/jsonb_test?data->a=eq.{\"b\":2}" `shouldRespondWith`
[json| [{"id":1,"data":{"a": {"b": 2}}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/jsonb_test?data->c=eq.[1,2,3]" `shouldRespondWith`
[json| [{"id":2,"data":{"c": [1, 2, 3]}}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/jsonb_test?data->0=eq.{\"d\":\"test\"}" `shouldRespondWith`
[json| [{"id":3,"data":[{"d": "test"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
context "ordering response" $ do
it "orders by a json column property asc" $
get "/json?order=data->>id.asc" `shouldRespondWith`
[json| [{"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}, {"data": {"id": 3}}] |]
{ matchHeaders = [matchContentTypeJson] }
it "orders by a json column with two level property nulls first" $
get "/json?order=data->foo->>bar.nullsfirst" `shouldRespondWith`
[json| [{"data": {"id": 3}}, {"data": {"id": 0}}, {"data": {"id": 1, "foo": {"bar": "baz"}}}] |]
{ matchHeaders = [matchContentTypeJson] }
context "Patching record, in a nonempty table" $
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` [json| [{ "data": { "escaped":" \"bar" } }] |]
{ matchStatus = 200 , matchHeaders = [] }
+25
View File
@@ -0,0 +1,25 @@
module Feature.NoJwtSpec where
-- {{{ Imports
import Test.Hspec
import Test.Hspec.Wai
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude
-- }}}
spec :: SpecWith Application
spec = describe "server started without JWT secret" $ do
-- this test will stop working 9999999999s after the UNIX EPOCH
it "responds with error on attempted auth" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjk5OTk5OTk5OTksInJvbGUiOiJwb3N0Z3Jlc3RfdGVzdF9hdXRob3IiLCJpZCI6Impkb2UifQ.Dpss-QoLYjec5OTsOaAc3FNVsSjA89wACoV-0ra3ClA"
request methodGet "/authors_only" [auth] ""
`shouldRespondWith` 500
it "behaves normally when user does not attempt auth" $
request methodGet "/items" [] ""
`shouldRespondWith` 200
+15
View File
@@ -0,0 +1,15 @@
module Feature.NonexistentSchemaSpec where
import Network.Wai (Application)
import Protolude hiding (get)
import Test.Hspec
import Test.Hspec.Wai
spec :: SpecWith Application
spec =
describe "Non existent api schema" $ do
it "succeeds when requesting root path" $
get "/" `shouldRespondWith` 200
it "gives 404 when requesting a nonexistent table in this nonexistent schema" $
get "/nonexistent_table" `shouldRespondWith` 404
+55
View File
@@ -0,0 +1,55 @@
module Feature.PgVersion95Spec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec = describe "features supported on PostgreSQL 9.5" $
context "json array negative index" $ do
it "can select with negative indexes" $ do
get "/json_arr?select=data->>-1::int&id=in.(1,2)" `shouldRespondWith`
[json| [{"data":3}, {"data":6}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->0->>-2::int&id=in.(3,4)" `shouldRespondWith`
[json| [{"data":8}, {"data":7}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->-2->>a&id=in.(5,6)" `shouldRespondWith`
[json| [{"a":"A"}, {"a":"[1,2,3]"}] |]
{ matchHeaders = [matchContentTypeJson] }
it "can filter with negative indexes" $ do
get "/json_arr?select=data&data->>-3=eq.1" `shouldRespondWith`
[json| [{"data":[1, 2, 3]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->-1->>-3=eq.11" `shouldRespondWith`
[json| [{"data":[[9, 8, 7], [11, 12, 13]]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->-1->>b=eq.B" `shouldRespondWith`
[json| [{"data":[{"a": "A"}, {"b": "B"}]}] |]
{ matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data&data->-1->b->>-1=eq.5" `shouldRespondWith`
[json| [{"data":[{"a": [1,2,3]}, {"b": [4,5]}]}] |]
{ matchHeaders = [matchContentTypeJson] }
it "should fail on badly formed negatives" $ do
get "/json_arr?select=data->>-78xy" `shouldRespondWith`
[json|
{"details": "unexpected 'x' expecting digit, \"->\", \"::\" or end of input",
"message": "\"failed to parse select parameter (data->>-78xy)\" (line 1, column 11)"} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->>--34" `shouldRespondWith`
[json|
{"details": "unexpected \"-\" expecting digit",
"message": "\"failed to parse select parameter (data->>--34)\" (line 1, column 9)"} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
get "/json_arr?select=data->>-xy-4" `shouldRespondWith`
[json|
{"details":"unexpected \"x\" expecting digit",
"message":"\"failed to parse select parameter (data->>-xy-4)\" (line 1, column 9)"} |]
{ matchStatus = 400, matchHeaders = [matchContentTypeJson] }
+81
View File
@@ -0,0 +1,81 @@
module Feature.PgVersion96Spec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "features supported on PostgreSQL 9.6" $ do
context "GUC headers" $ do
it "succeeds setting the headers" $ do
get "/rpc/get_projects_and_guc_headers?id=eq.2&select=id"
`shouldRespondWith` [json|[{"id": 2}]|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
get "/rpc/get_int_and_guc_headers?num=1"
`shouldRespondWith` [json|1|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
post "/rpc/get_int_and_guc_headers" [json|{"num": 1}|]
`shouldRespondWith` [json|1|]
{matchHeaders = [
matchContentTypeJson,
"X-Test" <:> "key1=val1; someValue; key2=val2",
"X-Test-2" <:> "key1=val1"]}
it "fails when setting headers with wrong json structure" $ do
get "/rpc/bad_guc_headers_1" `shouldRespondWith` 500
get "/rpc/bad_guc_headers_2" `shouldRespondWith` 500
get "/rpc/bad_guc_headers_3" `shouldRespondWith` 500
post "/rpc/bad_guc_headers_1" [json|{}|] `shouldRespondWith` 500
it "can set the same http header twice" $
get "/rpc/set_cookie_twice"
`shouldRespondWith` "null"
{matchHeaders = [
matchContentTypeJson,
"Set-Cookie" <:> "sessionid=38afes7a8; HttpOnly; Path=/",
"Set-Cookie" <:> "id=a3fWa; Expires=Wed, 21 Oct 2015 07:28:00 GMT; Secure; HttpOnly"]}
context "Use of the phraseto_tsquery function" $ do
it "finds matches" $
get "/tsearch?text_search_vector=phfts.The%20Fat%20Cats" `shouldRespondWith`
[json| [{"text_search_vector": "'ate':3 'cat':2 'fat':1 'rat':4" }] |]
{ matchHeaders = [matchContentTypeJson] }
it "finds matches with different dictionaries" $
get "/tsearch?text_search_vector=phfts(german).Art%20Spass" `shouldRespondWith`
[json| [{"text_search_vector": "'art':4 'spass':5 'unmog':7" }] |]
{ matchHeaders = [matchContentTypeJson] }
it "can be negated with not operator" $
get "/tsearch?text_search_vector=not.phfts(english).The%20Fat%20Cats" `shouldRespondWith`
[json| [
{"text_search_vector": "'fun':5 'imposs':9 'kind':3"},
{"text_search_vector": "'also':2 'fun':3 'possibl':8"},
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4"},
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can be used with or query param" $
get "/tsearch?or=(text_search_vector.phfts(german).Art%20Spass, text_search_vector.phfts(french).amusant, text_search_vector.fts(english).impossible)" `shouldRespondWith`
[json|[
{"text_search_vector": "'fun':5 'imposs':9 'kind':3" },
{"text_search_vector": "'amus':5 'fair':7 'impossibl':9 'peu':4" },
{"text_search_vector": "'art':4 'spass':5 'unmog':7"}
]|] { matchHeaders = [matchContentTypeJson] }
it "should work when used with GET RPC" $
get "/rpc/get_tsearch?text_search_vector=phfts(english).impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] }
+15
View File
@@ -0,0 +1,15 @@
module Feature.ProxySpec where
import Test.Hspec
import SpecHelper
import Network.Wai (Application)
import Protolude
spec :: SpecWith Application
spec =
describe "GET / with proxy" $
it "returns a valid openapi spec with proxy" $
validateOpenApiResponse [("Accept", "application/openapi+json")]
+43
View File
@@ -0,0 +1,43 @@
module Feature.QueryLimitedSpec 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 SpecHelper
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "Requesting many items with server limits enabled" $ do
it "restricts results" $
get "/items"
`shouldRespondWith` [json| [{"id":1},{"id":2}] |]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
it "respects additional client limiting" $ do
r <- request methodGet "/items"
(rangeHdrs $ ByteRangeFromTo 0 0) ""
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-0/*"
simpleStatus r `shouldBe` ok200
it "limit works on all levels" $
get "/users?select=id,tasks(id)&order=id.asc&tasks.order=id.asc"
`shouldRespondWith` [json|[{"id":1,"tasks":[{"id":1},{"id":2}]},{"id":2,"tasks":[{"id":5},{"id":6}]}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
it "limit is not applied to parent embeds" $
get "/tasks?select=id,project(id)&id=gt.5"
`shouldRespondWith` [json|[{"id":6,"project":{"id":3}},{"id":7,"project":{"id":4}}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
+890 -112
View File
File diff suppressed because it is too large Load Diff
+167 -23
View File
@@ -2,21 +2,168 @@ 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)
import Protolude hiding (get)
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" [] emptyRange
`shouldRespondWith` [json| [] |] {matchHeaders = ["Content-Range" <:> "*/*"]}
it "returns range Content-Range with range/*" $
request methodPost "/rpc/getitemrange" [] defaultRange
`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}] |]
{ 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/*"
simpleStatus r `shouldBe` ok200
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` "[]"
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
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/*"
simpleStatus r `shouldBe` ok200
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/*"
simpleStatus r `shouldBe` ok200
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"
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) emptyRange
`shouldRespondWith` "[]"
{ matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"]
}
it "refuses a range requesting start past last item" $
request methodPost "/rpc/getitemrange"
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) defaultRange
`shouldRespondWith` "[]"
{ matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/15"]
}
spec :: Spec
spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable "items")
. around withApp $
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` "[]"
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "returns range Content-Range with range/*" $
request methodGet "/items?order=id"
[("Prefer", "count=none")] ""
`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}] |]
{ 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` [json| [{"id":1}] |]
{ matchHeaders = ["Content-Range" <:> "0-0/*"] }
context "with limit/offset parameters" $ do
it "no parameters return everything" $
get "/items?select=id&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}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-14/*"]
}
it "top level limit with parameter" $
get "/items?select=id&order=id.asc&limit=3"
`shouldRespondWith` [json|[{"id":1},{"id":2},{"id":3}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-2/*"]
}
it "headers override get parameters" $
request methodGet "/items?select=id&order=id.asc&limit=3"
(rangeHdrs $ ByteRangeFromTo 0 1) ""
`shouldRespondWith` [json|[{"id":1},{"id":2}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-1/*"]
}
it "limit works on all levels" $
get "/clients?select=id,projects(id,tasks(id))&order=id.asc&limit=1&projects.order=id.asc&projects.limit=2&projects.tasks.order=id.asc&projects.tasks.limit=1"
`shouldRespondWith`
[json|[{"id":1,"projects":[{"id":1,"tasks":[{"id":1}]},{"id":2,"tasks":[{"id":3}]}]}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
it "limit and offset works on first level" $
get "/items?select=id&order=id.asc&limit=3&offset=2"
`shouldRespondWith` [json|[{"id":3},{"id":4},{"id":5}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "2-4/*"]
}
context "with range headers" $ do
context "of acceptable range" $ do
@@ -25,8 +172,8 @@ spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable
(rangeHdrs $ ByteRangeFromTo 0 1) ""
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-1/15"
simpleStatus r `shouldBe` partialContent206
matchHeader "Content-Range" "0-1/*"
simpleStatus r `shouldBe` ok200
it "understands open-ended ranges" $
request methodGet "/items"
@@ -36,10 +183,9 @@ spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable
it "returns an empty body when there are no results" $
request methodGet "/menagerie"
(rangeHdrs $ ByteRangeFromTo 0 1) ""
`shouldRespondWith` ResponseMatcher {
matchBody = Just "[]"
, matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/0"]
`shouldRespondWith` "[]"
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "allows one-item requests" $ do
@@ -47,16 +193,16 @@ spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable
(rangeHdrs $ ByteRangeFromTo 0 0) ""
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "0-0/15"
simpleStatus r `shouldBe` partialContent206
matchHeader "Content-Range" "0-0/*"
simpleStatus r `shouldBe` ok200
it "handles ranges beyond collection length via truncation" $ do
r <- request methodGet "/items"
(rangeHdrs $ ByteRangeFromTo 10 100) ""
liftIO $ do
simpleHeaders r `shouldSatisfy`
matchHeader "Content-Range" "10-14/15"
simpleStatus r `shouldBe` partialContent206
matchHeader "Content-Range" "10-14/*"
simpleStatus r `shouldBe` ok200
context "of invalid range" $ do
it "fails with 416 for offside range" $
@@ -66,18 +212,16 @@ spec = beforeAll (clearTable "items" >> createItems 15) . afterAll_ (clearTable
it "refuses a range with nonzero start when there are no items" $
request methodGet "/menagerie"
(rangeHdrs $ ByteRangeFromTo 1 2) ""
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 416
(rangeHdrsWithCount $ ByteRangeFromTo 1 2) ""
`shouldRespondWith` "[]"
{ matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/0"]
}
it "refuses a range requesting start past last item" $
request methodGet "/items"
(rangeHdrs $ ByteRangeFromTo 100 199) ""
`shouldRespondWith` ResponseMatcher {
matchBody = Nothing
, matchStatus = 416
(rangeHdrsWithCount $ ByteRangeFromTo 100 199) ""
`shouldRespondWith` "[]"
{ matchStatus = 416
, matchHeaders = ["Content-Range" <:> "*/15"]
}
+378
View File
@@ -0,0 +1,378 @@
module Feature.RpcSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleStatus, simpleBody))
import qualified Data.ByteString.Lazy as BL (empty)
import SpecHelper
import Text.Heredoc
import Network.Wai (Application)
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "remote procedure call" $ do
context "a proc that returns a set" $ do
it "returns paginated results" $ do
request methodPost "/rpc/getitemrange"
(rangeHdrs (ByteRangeFromTo 0 0)) [json| { "min": 2, "max": 4 } |]
`shouldRespondWith` [json| [{"id":3}] |]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
request methodGet "/rpc/getitemrange?min=2&max=4"
(rangeHdrs (ByteRangeFromTo 0 0)) ""
`shouldRespondWith` [json| [{"id":3}] |]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-0/*"]
}
it "includes total count if requested" $ do
request methodPost "/rpc/getitemrange"
(rangeHdrsWithCount (ByteRangeFromTo 0 0))
[json| { "min": 2, "max": 4 } |]
`shouldRespondWith` [json| [{"id":3}] |]
{ matchStatus = 206 -- it now knows the response is partial
, matchHeaders = ["Content-Range" <:> "0-0/2"]
}
request methodGet "/rpc/getitemrange?min=2&max=4"
(rangeHdrsWithCount (ByteRangeFromTo 0 0)) ""
`shouldRespondWith` [json| [{"id":3}] |]
{ matchStatus = 206
, matchHeaders = ["Content-Range" <:> "0-0/2"]
}
it "returns proper json" $ do
post "/rpc/getitemrange" [json| { "min": 2, "max": 4 } |] `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/getitemrange?min=2&max=4" `shouldRespondWith`
[json| [ {"id": 3}, {"id":4} ] |]
{ matchHeaders = [matchContentTypeJson] }
it "returns CSV" $ do
request methodPost "/rpc/getitemrange"
(acceptHdrs "text/csv")
[json| { "min": 2, "max": 4 } |]
`shouldRespondWith` "id\n3\n4"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
}
request methodGet "/rpc/getitemrange?min=2&max=4"
(acceptHdrs "text/csv") ""
`shouldRespondWith` "id\n3\n4"
{ matchStatus = 200
, matchHeaders = ["Content-Type" <:> "text/csv; charset=utf-8"]
}
context "unknown function" $ do
it "returns 404" $
post "/rpc/fakefunc" [json| {} |] `shouldRespondWith` 404
it "should fail with 404 on unknown proc name" $
get "/rpc/fake" `shouldRespondWith` 404
it "should fail with 404 on unknown proc args" $ do
get "/rpc/sayhello" `shouldRespondWith` 404
get "/rpc/sayhello?any_arg=value" `shouldRespondWith` 404
it "works when having uppercase identifiers" $ do
get "/rpc/quotedFunction?user=mscott&fullName=Michael Scott&SSN=401-32-XXXX" `shouldRespondWith`
[json|{"user": "mscott", "fullName": "Michael Scott", "SSN": "401-32-XXXX"}|]
{ matchHeaders = [matchContentTypeJson] }
post "/rpc/quotedFunction"
[json|{"user": "dschrute", "fullName": "Dwight Schrute", "SSN": "030-18-XXXX"}|]
`shouldRespondWith`
[json|{"user": "dschrute", "fullName": "Dwight Schrute", "SSN": "030-18-XXXX"}|]
{ matchHeaders = [matchContentTypeJson] }
context "shaping the response returned by a proc" $ do
it "returns a project" $ do
post "/rpc/getproject" [json| { "id": 1} |] `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1}]|]
get "/rpc/getproject?id=1" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7","client_id":1}]|]
it "can filter proc results" $ do
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" [json| {} |] `shouldRespondWith`
[json|[{"id":2},{"id":3},{"id":4}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id" `shouldRespondWith`
[json|[{"id":2},{"id":3},{"id":4}]|]
{ matchHeaders = [matchContentTypeJson] }
it "can limit proc results" $ do
post "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1" [json| {} |]
`shouldRespondWith` [json|[{"id":3},{"id":4}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "1-2/*"] }
get "/rpc/getallprojects?id=gt.1&id=lt.5&select=id?limit=2&offset=1"
`shouldRespondWith` [json|[{"id":3},{"id":4}]|]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "1-2/*"] }
it "select works on the first level" $ do
post "/rpc/getproject?select=id,name" [json| { "id": 1} |] `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7"}]|]
get "/rpc/getproject?id=1&select=id,name" `shouldRespondWith`
[str|[{"id":1,"name":"Windows 7"}]|]
context "foreign entities embedding" $ do
it "can embed if related tables are in the exposed schema" $ do
post "/rpc/getproject?select=id,name,client(id),tasks(id)" [json| { "id": 1} |] `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/getproject?id=1&select=id,name,client(id),tasks(id)" `shouldRespondWith`
[json|[{"id":1,"name":"Windows 7","client":{"id":1},"tasks":[{"id":1},{"id":2}]}]|]
{ matchHeaders = [matchContentTypeJson] }
it "cannot embed if the related table is not in the exposed schema" $ do
post "/rpc/single_article?select=*,article_stars(*)" [json|{ "id": 1}|]
`shouldRespondWith` 400
get "/rpc/single_article?id=1&select=*,article_stars(*)"
`shouldRespondWith` 400
it "can embed if the related tables are in a hidden schema but exposed as views" $ do
post "/rpc/single_article?select=id,articleStars(userId)" [json|{ "id": 2}|]
`shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/single_article?id=2&select=id,articleStars(userId)"
`shouldRespondWith` [json|[{"id": 2, "articleStars": [{"userId": 3}]}]|]
{ matchHeaders = [matchContentTypeJson] }
context "a proc that returns an empty rowset" $
it "returns empty json array" $ do
post "/rpc/test_empty_rowset" [json| {} |] `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/test_empty_rowset" `shouldRespondWith`
[json| [] |]
{ matchHeaders = [matchContentTypeJson] }
context "proc return types" $ do
context "returns text" $ do
it "returns proper json" $
post "/rpc/sayhello" [json| { "name": "world" } |] `shouldRespondWith`
[json|"Hello, world"|]
{ matchHeaders = [matchContentTypeJson] }
it "can handle unicode" $
post "/rpc/sayhello" [json| { "name": "" } |] `shouldRespondWith`
[json|"Hello, ¥"|]
{ matchHeaders = [matchContentTypeJson] }
it "returns array" $
post "/rpc/ret_array" [json|{}|] `shouldRespondWith`
[json|[1, 2, 3]|]
{ matchHeaders = [matchContentTypeJson] }
it "returns setof integers" $
post "/rpc/ret_setof_integers" [json|{}|] `shouldRespondWith`
[json|[{ "ret_setof_integers": 1 },
{ "ret_setof_integers": 2 },
{ "ret_setof_integers": 3 }]|]
{ matchHeaders = [matchContentTypeJson] }
it "returns enum value" $
post "/rpc/ret_enum" [json|{ "val": "foo" }|] `shouldRespondWith`
[json|"foo"|]
{ matchHeaders = [matchContentTypeJson] }
it "returns domain value" $
post "/rpc/ret_domain" [json|{ "val": "8" }|] `shouldRespondWith`
[json|8|]
{ matchHeaders = [matchContentTypeJson] }
it "returns range" $
post "/rpc/ret_range" [json|{ "low": 10, "up": 20 }|] `shouldRespondWith`
[json|"[10,20)"|]
{ matchHeaders = [matchContentTypeJson] }
it "returns row of scalars" $
post "/rpc/ret_scalars" [json|{}|] `shouldRespondWith`
[json|[{"a":"scalars", "b":"foo", "c":1, "d":"[10,20)"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "returns composite type in exposed schema" $
post "/rpc/ret_point_2d" [json|{}|] `shouldRespondWith`
[json|[{"x": 10, "y": 5}]|]
{ matchHeaders = [matchContentTypeJson] }
it "cannot return composite type in hidden schema" $
post "/rpc/ret_point_3d" [json|{}|] `shouldRespondWith` 401
it "returns single row from table" $
post "/rpc/single_article?select=id" [json|{"id": 2}|] `shouldRespondWith`
[json|[{"id": 2}]|]
{ matchHeaders = [matchContentTypeJson] }
it "returns null for void" $
post "/rpc/ret_void" [json|{}|] `shouldRespondWith`
[json|null|]
{ matchHeaders = [matchContentTypeJson] }
context "improper input" $ do
it "rejects unknown content type even if payload is good" $ do
request methodPost "/rpc/sayhello"
(acceptHdrs "audio/mpeg3") [json| { "name": "world" } |]
`shouldRespondWith` 415
request methodGet "/rpc/sayhello?name=world"
(acceptHdrs "audio/mpeg3") ""
`shouldRespondWith` 415
it "rejects malformed json payload" $ do
p <- request methodPost "/rpc/sayhello"
(acceptHdrs "application/json") "sdfsdf"
liftIO $ do
simpleStatus p `shouldBe` badRequest400
isErrorFormat (simpleBody p) `shouldBe` True
it "treats simple plpgsql raise as invalid input" $ do
p <- post "/rpc/problem" "{}"
liftIO $ do
simpleStatus p `shouldBe` badRequest400
isErrorFormat (simpleBody p) `shouldBe` True
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 "executes the proc exactly once per request" $ do
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
[json|1|]
{ matchHeaders = [matchContentTypeJson] }
post "/rpc/callcounter" [json| {} |] `shouldRespondWith`
[json|2|]
{ matchHeaders = [matchContentTypeJson] }
context "a proc that receives no parameters" $ do
it "interprets empty string as empty json object on a post request" $
post "/rpc/noparamsproc" BL.empty `shouldRespondWith`
[json| "Return value of no parameters procedure." |]
{ matchHeaders = [matchContentTypeJson] }
it "interprets empty string as a function with no args on a get request" $
get "/rpc/noparamsproc" `shouldRespondWith`
[json| "Return value of no parameters procedure." |]
{ matchHeaders = [matchContentTypeJson] }
it "returns proper output when having the same return col name as the proc name" $ do
post "/rpc/test" [json|{}|] `shouldRespondWith`
[json|[{"test":"hello","value":1}]|] { matchHeaders = [matchContentTypeJson] }
get "/rpc/test" `shouldRespondWith`
[json|[{"test":"hello","value":1}]|] { matchHeaders = [matchContentTypeJson] }
context "procs with OUT/INOUT params" $ do
it "returns a scalar result when there is a single OUT param" $ do
get "/rpc/single_out_param?num=5" `shouldRespondWith`
[json|6|] { matchHeaders = [matchContentTypeJson] }
get "/rpc/single_json_out_param?a=1&b=two" `shouldRespondWith`
[json|{"a": 1, "b": "two"}|] { matchHeaders = [matchContentTypeJson] }
it "returns a scalar result when there is a single INOUT param" $
get "/rpc/single_inout_param?num=2" `shouldRespondWith`
[json|3|] { matchHeaders = [matchContentTypeJson] }
it "returns a row result when there are many OUT params" $
get "/rpc/many_out_params" `shouldRespondWith`
[json|[{"my_json":{"a": 1, "b": "two"},"num":3,"str":"four"}]|] { matchHeaders = [matchContentTypeJson] }
it "returns a row result when there are many INOUT params" $
get "/rpc/many_inout_params?num=1&str=two&b=false" `shouldRespondWith`
[json| [{"num":1,"str":"two","b":false}]|] { matchHeaders = [matchContentTypeJson] }
it "can handle procs with args that have a DEFAULT value" $ do
get "/rpc/many_inout_params?num=1&str=two" `shouldRespondWith`
[json| [{"num":1,"str":"two","b":true}]|] { matchHeaders = [matchContentTypeJson] }
get "/rpc/three_defaults?b=4" `shouldRespondWith`
[json|8|] { matchHeaders = [matchContentTypeJson] }
it "can map a RAISE error code and message to a http status" $
get "/rpc/raise_pt402"
`shouldRespondWith` [json|{ "hint": "Upgrade your plan", "details": "Quota exceeded" }|]
{ matchStatus = 402
, matchHeaders = [matchContentTypeJson]
}
it "defaults to status 500 if RAISE code is PT not followed by a number" $
get "/rpc/raise_bad_pt" `shouldRespondWith` 500
context "expects a single json object" $ do
it "does not expand posted json into parameters" $
request methodPost "/rpc/singlejsonparam"
[("prefer","params=single-object")] [json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |] `shouldRespondWith`
[json| { "p1": 1, "p2": "text", "p3" : {"obj":"text"} } |]
{ matchHeaders = [matchContentTypeJson] }
it "accepts parameters from an html form" $
request methodPost "/rpc/singlejsonparam"
[("Prefer","params=single-object"),("Content-Type", "application/x-www-form-urlencoded")]
("integer=7&double=2.71828&varchar=forms+are+fun&" <>
"boolean=false&date=1900-01-01&money=$3.99&enum=foo") `shouldRespondWith`
[json| { "integer": "7", "double": "2.71828", "varchar" : "forms are fun"
, "boolean":"false", "date":"1900-01-01", "money":"$3.99", "enum":"foo" } |]
{ matchHeaders = [matchContentTypeJson] }
it "works with GET" $
request methodGet "/rpc/singlejsonparam?p1=1&p2=text" [("Prefer","params=single-object")] ""
`shouldRespondWith` [json|{ "p1": "1", "p2": "text"}|]
{ matchHeaders = [matchContentTypeJson] }
it "should work with an overloaded function" $ do
get "/rpc/overloaded" `shouldRespondWith`
[json|[{ "overloaded": 1 },
{ "overloaded": 2 },
{ "overloaded": 3 }]|]
{ matchHeaders = [matchContentTypeJson] }
request methodPost "/rpc/overloaded" [("Prefer","params=single-object")]
[json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]
`shouldRespondWith`
[json|[{"x": 1, "y": "first"}, {"x": 2, "y": "second"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/overloaded?a=1&b=2" `shouldRespondWith` [str|3|]
get "/rpc/overloaded?a=1&b=2&c=3" `shouldRespondWith` [str|"123"|]
context "only for POST rpc" $
it "gives a parse filter error if GET style proc args are specified" $
post "/rpc/sayhello?name=John" [json|{}|] `shouldRespondWith` 400
context "only for GET rpc" $ do
it "should fail on mutating procs" $ do
get "/rpc/callcounter" `shouldRespondWith` 500
get "/rpc/setprojects?id_l=1&id_h=5&name=FreeBSD" `shouldRespondWith` 500
it "should filter a proc that has arg name = filter name" $
get "/rpc/get_projects_below?id=5&id=gt.2&select=id" `shouldRespondWith`
[json|[{ "id": 3 }, { "id": 4 }]|]
{ matchHeaders = [matchContentTypeJson] }
it "should work with filters that have the not operator" $ do
get "/rpc/get_projects_below?id=5&id=not.gt.2&select=id" `shouldRespondWith`
[json|[{ "id": 1 }, { "id": 2 }]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/get_projects_below?id=5&id=not.in.(1,3)&select=id" `shouldRespondWith`
[json|[{ "id": 2 }, { "id": 4 }]|]
{ matchHeaders = [matchContentTypeJson] }
it "should work with filters that use the plain with language fts operator" $ do
get "/rpc/get_tsearch?text_search_vector=fts(english).impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/get_tsearch?text_search_vector=plfts.impossible" `shouldRespondWith`
[json|[{"text_search_vector":"'fun':5 'imposs':9 'kind':3"}]|]
{ matchHeaders = [matchContentTypeJson] }
get "/rpc/get_tsearch?text_search_vector=not.fts(english).fun%7Crat" `shouldRespondWith`
[json|[{"text_search_vector":"'amus':5 'fair':7 'impossibl':9 'peu':4"},{"text_search_vector":"'art':4 'spass':5 'unmog':7"}]|]
{ matchHeaders = [matchContentTypeJson] }
it "should work with an argument of custom type in public schema" $
get "/rpc/test_arg?my_arg=something" `shouldRespondWith`
[json|"foobar"|]
{ matchHeaders = [matchContentTypeJson] }
+194
View File
@@ -0,0 +1,194 @@
module Feature.SingularSpec where
import Text.Heredoc
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import Network.Wai.Test (SResponse(..))
import Network.Wai (Application)
import SpecHelper
import Protolude hiding (get)
spec :: SpecWith Application
spec =
describe "Requesting singular json object" $ do
let pgrstObj = "application/vnd.pgrst.object+json"
singular = ("Accept", pgrstObj)
context "with GET request" $ do
it "fails for zero rows" $
request methodGet "/items?id=gt.0&id=lt.0" [singular] ""
`shouldRespondWith` 406
it "will select an existing object" $ do
request methodGet "/items?id=eq.5" [singular] ""
`shouldRespondWith` [str|{"id":5}|]
-- also test without the +json suffix
request methodGet "/items?id=eq.5"
[("Accept", "application/vnd.pgrst.object")] ""
`shouldRespondWith` [str|{"id":5}|]
it "can combine multiple prefer values" $
request methodGet "/items?id=eq.5" [singular, ("Prefer","count=none")] ""
`shouldRespondWith` [str|{"id":5}|]
it "can shape plurality singular object routes" $
request methodGet "/projects_view?id=eq.1&select=id,name,clients(*),tasks(id,name)" [singular] ""
`shouldRespondWith`
[json|{"id":1,"name":"Windows 7","clients":{"id":1,"name":"Microsoft"},"tasks":[{"id":1,"name":"Design w7"},{"id":2,"name":"Code w7"}]}|]
{ matchHeaders = ["Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"] }
context "when updating rows" $ do
it "works for one row" $ do
_ <- post "/addresses" [json| { id: 97, address: "A Street" } |]
request methodPatch
"/addresses?id=eq.97"
[("Prefer", "return=representation"), singular]
[json| { address: "B Street" } |]
`shouldRespondWith`
[str|{"id":97,"address":"B Street"}|]
it "raises an error for multiple rows" $ do
_ <- post "/addresses" [json| { id: 98, address: "xxx" } |]
_ <- post "/addresses" [json| { id: 99, address: "yyy" } |]
p <- request methodPatch
"/addresses?id=gt.0"
[("Prefer", "return=representation"), singular]
[json| { address: "zzz" } |]
liftIO $ do
simpleStatus p `shouldBe` notAcceptable406
isErrorFormat (simpleBody p) `shouldBe` True
-- the rows should not be updated, either
get "/addresses?id=eq.98" `shouldRespondWith` [str|[{"id":98,"address":"xxx"}]|]
it "raises an error for zero rows" $ do
p <- request methodPatch "/items?id=gt.0&id=lt.0"
[("Prefer", "return=representation"), singular] [json|{"id":1}|]
liftIO $ do
simpleStatus p `shouldBe` notAcceptable406
isErrorFormat (simpleBody p) `shouldBe` True
context "when creating rows" $ do
it "works for one row" $ do
p <- request methodPost
"/addresses"
[("Prefer", "return=representation"), singular]
[json| [ { id: 100, address: "xxx" } ] |]
liftIO $ simpleBody p `shouldBe` [str|{"id":100,"address":"xxx"}|]
it "works for one row even with return=minimal" $ do
request methodPost "/addresses"
[("Prefer", "return=minimal"), singular]
[json| [ { id: 101, address: "xxx" } ] |]
`shouldRespondWith` ""
{ matchStatus = 201
, matchHeaders = ["Content-Range" <:> "*/*"]
}
-- and the element should exist
get "/addresses?id=eq.101"
`shouldRespondWith` [str|[{"id":101,"address":"xxx"}]|]
{ matchStatus = 200
, matchHeaders = []
}
it "raises an error when attempting to create multiple entities" $ do
p <- request methodPost
"/addresses"
[("Prefer", "return=representation"), singular]
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
liftIO $ simpleStatus p `shouldBe` notAcceptable406
-- the rows should not exist, either
get "/addresses?id=eq.200" `shouldRespondWith` "[]"
it "return=minimal allows request to create multiple elements" $
request methodPost "/addresses"
[("Prefer", "return=minimal"), singular]
[json| [ { id: 200, address: "xxx" }, { id: 201, address: "yyy" } ] |]
`shouldRespondWith` ""
{ matchStatus = 201
, matchHeaders = ["Content-Range" <:> "*/*"]
}
it "raises an error when creating zero entities" $ do
p <- request methodPost
"/addresses"
[("Prefer", "return=representation"), singular]
[json| [ ] |]
liftIO $ do
simpleStatus p `shouldBe` notAcceptable406
isErrorFormat (simpleBody p) `shouldBe` True
context "when deleting rows" $ do
it "works for one row" $ do
p <- request methodDelete
"/items?id=eq.11"
[("Prefer", "return=representation"), singular] ""
liftIO $ simpleBody p `shouldBe` [str|{"id":11}|]
it "raises an error when attempting to delete multiple entities" $ do
let firstItems = "/items?id=gt.0&id=lt.11"
request methodDelete firstItems
[("Prefer", "return=representation"), singular] ""
`shouldRespondWith` 406
get firstItems
`shouldRespondWith` [json| [{"id":1},{"id":2},{"id":3},{"id":4},{"id":5},{"id":6},{"id":7},{"id":8},{"id":9},{"id":10}] |]
{ matchStatus = 200
, matchHeaders = ["Content-Range" <:> "0-9/*"]
}
it "raises an error when deleting zero entities" $ do
p <- request methodDelete "/items?id=lt.0"
[("Prefer", "return=representation"), singular] ""
liftIO $ do
simpleStatus p `shouldBe` notAcceptable406
isErrorFormat (simpleBody p) `shouldBe` True
context "when calling a stored proc" $ do
it "fails for zero rows" $ do
p <- request methodPost "/rpc/getproject"
[singular] [json|{ "id": 9999999}|]
liftIO $ do
simpleStatus p `shouldBe` notAcceptable406
isErrorFormat (simpleBody p) `shouldBe` True
-- this one may be controversial, should vnd.pgrst.object include
-- the likes of 2 and "hello?"
it "succeeds for scalar result" $
request methodPost "/rpc/sayhello"
[singular] [json|{ "name": "world"}|]
`shouldRespondWith` 200
it "returns a single object for json proc" $
request methodPost "/rpc/getproject"
[singular] [json|{ "id": 1}|] `shouldRespondWith`
[str|{"id":1,"name":"Windows 7","client_id":1}|]
it "fails for multiple rows" $ do
p <- request methodPost "/rpc/getallprojects" [singular] "{}"
liftIO $ do
simpleStatus p `shouldBe` notAcceptable406
isErrorFormat (simpleBody p) `shouldBe` True
it "executes the proc exactly once per request" $ do
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
`shouldRespondWith` [str|[{"id":1,"name":"Windows 7"}]|]
p <- request methodPost "/rpc/setprojects" [singular]
[json| {"id_l": 1, "id_h": 2, "name": "changed"} |]
liftIO $ do
simpleStatus p `shouldBe` notAcceptable406
isErrorFormat (simpleBody p) `shouldBe` True
-- should not actually have executed the function
request methodPost "/rpc/getproject?select=id,name" [] [json| {"id": 1} |]
`shouldRespondWith` [str|[{"id":1,"name":"Windows 7"}]|]
+314 -181
View File
@@ -1,194 +1,327 @@
module Feature.StructureSpec where
import Test.Hspec hiding (pendingWith)
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import PostgREST.Config (docsVersion)
import Control.Lens ((^?))
import Data.Aeson.Types (Value (..))
import Data.Aeson.Lens
import Data.Aeson.QQ
import SpecHelper
import Network.HTTP.Types
import Network.Wai (Application)
import Network.Wai.Test (SResponse(..))
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_count_column","insertable":false}
, {"schema":"1","name":"has_fk","insertable":true}
, {"schema":"1","name":"insertable_view_with_join","insertable":true}
, {"schema":"1","name":"items","insertable":true}
, {"schema":"1","name":"json","insertable":true}
, {"schema":"1","name":"materialized_view","insertable":false}
, {"schema":"1","name":"menagerie","insertable":true}
, {"schema":"1","name":"no_pk","insertable":true}
, {"schema":"1","name":"nullable_integer","insertable":true}
, {"schema":"1","name":"simple_pk","insertable":true}
, {"schema":"1","name":"tsearch","insertable":true}
] |]
{matchStatus = 200}
import Protolude hiding (get)
it "lists only views user has permission to see" $ do
_ <- post "/postgrest/users" [json| { "id":"jdoe", "pass": "1234", "role": "postgrest_test_author" } |]
let auth = authHeaderBasic "jdoe" "1234"
spec :: SpecWith Application
spec = do
request methodGet "/" [auth] ""
`shouldRespondWith` [json| [
{"schema":"1","name":"authors_only","insertable":true}
] |]
{matchStatus = 200}
describe "OpenAPI" $ do
it "root path returns a valid openapi spec" $
validateOpenApiResponse [("Accept", "application/openapi+json")]
it "should respond to openapi request on none root path with 415" $
request methodGet "/items"
(acceptHdrs "application/openapi+json") ""
`shouldRespondWith` 415
describe "Table info" $ do
it "is available with OPTIONS verb" $
request methodOptions "/menagerie" [] "" `shouldRespondWith`
[json|
{
"pkey":["integer"],
"columns":[
{
"default": null,
"precision": 32,
"updatable": true,
"schema": "1",
"name": "integer",
"type": "integer",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 1,
"references": null,
"default": null
}, {
"default": null,
"precision": 53,
"updatable": true,
"schema": "1",
"name": "double",
"type": "double precision",
"maxLen": null,
"enum": [],
"nullable": false,
"references": null,
"position": 2
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"name": "varchar",
"type": "character varying",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 3,
"references": null,
"default": null
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"name": "boolean",
"type": "boolean",
"maxLen": null,
"enum": [],
"nullable": false,
"references": null,
"position": 4
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"name": "date",
"type": "date",
"maxLen": null,
"enum": [],
"nullable": false,
"references": null,
"position": 5
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"name": "money",
"type": "money",
"maxLen": null,
"enum": [],
"nullable": false,
"position": 6,
"references": null,
"default": null
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"name": "enum",
"type": "USER-DEFINED",
"maxLen": null,
"enum": [
"foo",
"bar"
],
"nullable": false,
"position": 7,
"references": null,
"default": null
}
]
}
|]
it "includes postgrest.org current version api docs" $ do
r <- simpleBody <$> get "/"
it "includes foreign key data" $ do
pendingWith "have to resolve issue #107"
let docsUrl = r ^? key "externalDocs" . key "url"
request methodOptions "/has_fk" [] ""
`shouldRespondWith` [json|
{
"pkey": ["id"],
"columns":[
{
"default": "nextval('\"1\".has_fk_id_seq'::regclass)",
"precision": 64,
"updatable": true,
"schema": "1",
"name": "id",
"type": "bigint",
"maxLen": null,
"nullable": false,
"position": 1,
"enum": [],
"references": null
}, {
"default": null,
"precision": 32,
"updatable": true,
"schema": "1",
"name": "auto_inc_fk",
"type": "integer",
"maxLen": null,
"nullable": true,
"position": 2,
"enum": [],
"references": {"table": "auto_incrementing_pk", "column": "id"}
}, {
"default": null,
"precision": null,
"updatable": true,
"schema": "1",
"name": "simple_fk",
"type": "character varying",
"maxLen": 255,
"nullable": true,
"position": 3,
"enum": [],
"references": {"table": "simple_pk", "column": "k"}
}
]
}
|]
liftIO $ docsUrl `shouldBe` Just (String ("https://postgrest.org/en/" <> docsVersion <> "/api.html"))
describe "table" $ do
it "includes paths to tables" $ do
r <- simpleBody <$> get "/"
let method s = key "paths" . key "/child_entities" . key s
childGetSummary = r ^? method "get" . key "summary"
childGetDescription = r ^? method "get" . key "description"
getParameters = r ^? method "get" . key "parameters"
postResponse = r ^? method "post" . key "responses" . key "201" . key "description"
patchResponse = r ^? method "patch" . key "responses" . key "204" . key "description"
deleteResponse = r ^? method "delete" . key "responses" . key "204" . key "description"
let grandChildGet s = key "paths" . key "/grandchild_entities" . key "get" . key s
grandChildGetSummary = r ^? grandChildGet "summary"
grandChildGetDescription = r ^? grandChildGet "description"
liftIO $ do
childGetSummary `shouldBe` Just "child_entities comment"
childGetDescription `shouldBe` Nothing
grandChildGetSummary `shouldBe` Just "grandchild_entities summary"
grandChildGetDescription `shouldBe` Just "grandchild_entities description\nthat spans\nmultiple lines"
getParameters `shouldBe` Just
[aesonQQ|
[
{ "$ref": "#/parameters/rowFilter.child_entities.id" },
{ "$ref": "#/parameters/rowFilter.child_entities.name" },
{ "$ref": "#/parameters/rowFilter.child_entities.parent_id" },
{ "$ref": "#/parameters/select" },
{ "$ref": "#/parameters/order" },
{ "$ref": "#/parameters/range" },
{ "$ref": "#/parameters/rangeUnit" },
{ "$ref": "#/parameters/offset" },
{ "$ref": "#/parameters/limit" },
{ "$ref": "#/parameters/preferCount" }
]
|]
postResponse `shouldBe` Just "Created"
patchResponse `shouldBe` Just "No Content"
deleteResponse `shouldBe` Just "No Content"
it "includes an array type for GET responses" $ do
r <- simpleBody <$> get "/"
let childGetSchema = r ^? key "paths"
. key "/child_entities"
. key "get"
. key "responses"
. key "200"
. key "schema"
liftIO $
childGetSchema `shouldBe` Just
[aesonQQ|
{
"items": {
"$ref": "#/definitions/child_entities"
},
"type": "array"
}
|]
it "includes definitions to tables" $ do
r <- simpleBody <$> get "/"
let def = r ^? key "definitions" . key "child_entities"
liftIO $
def `shouldBe` Just
[aesonQQ|
{
"type": "object",
"description": "child_entities comment",
"properties": {
"id": {
"description": "child_entities id comment\n\nNote:\nThis is a Primary Key.<pk/>",
"format": "integer",
"type": "integer"
},
"name": {
"description": "child_entities name comment",
"format": "text",
"type": "string"
},
"parent_id": {
"description": "Note:\nThis is a Foreign Key to `entities.id`.<fk table='entities' column='id'/>",
"format": "integer",
"type": "integer"
}
},
"required": [
"id"
]
}
|]
it "doesn't include privileged table for anonymous" $ do
r <- simpleBody <$> get "/"
let tablePath = r ^? key "paths" . key "/authors_only"
liftIO $ tablePath `shouldBe` Nothing
it "includes table if user has permission" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"
r <- simpleBody <$> request methodGet "/" [auth] ""
let tableTag = r ^? key "paths" . key "/authors_only"
. key "post" . key "tags"
. nth 0
liftIO $ tableTag `shouldBe` Just [aesonQQ|"authors_only"|]
describe "Foreign table" $
it "includes foreign table properties" $ do
r <- simpleBody <$> get "/"
let method s = key "paths" . key "/projects_dump" . key s
getSummary = r ^? method "get" . key "summary"
getDescription = r ^? method "get" . key "description"
getParameters = r ^? method "get" . key "parameters"
liftIO $ do
getSummary `shouldBe` Just "A temporary projects dump"
getDescription `shouldBe` Just "Just a test for foreign tables"
getParameters `shouldBe` Just
[aesonQQ|
[
{ "$ref": "#/parameters/rowFilter.projects_dump.id" },
{ "$ref": "#/parameters/rowFilter.projects_dump.name" },
{ "$ref": "#/parameters/rowFilter.projects_dump.client_id" },
{ "$ref": "#/parameters/select" },
{ "$ref": "#/parameters/order" },
{ "$ref": "#/parameters/range" },
{ "$ref": "#/parameters/rangeUnit" },
{ "$ref": "#/parameters/offset" },
{ "$ref": "#/parameters/limit" },
{ "$ref": "#/parameters/preferCount" }
]
|]
describe "Materialized view" $
it "includes materialized view properties" $ do
r <- simpleBody <$> get "/"
let method s = key "paths" . key "/materialized_projects" . key s
summary = r ^? method "get" . key "summary"
description = r ^? method "get" . key "description"
parameters = r ^? method "get" . key "parameters"
liftIO $ do
summary `shouldBe` Just "A materialized view for projects"
description `shouldBe` Just "Just a test for materialized views"
parameters `shouldBe` Just
[aesonQQ|
[
{ "$ref": "#/parameters/rowFilter.materialized_projects.id" },
{ "$ref": "#/parameters/rowFilter.materialized_projects.name" },
{ "$ref": "#/parameters/rowFilter.materialized_projects.client_id" },
{ "$ref": "#/parameters/select" },
{ "$ref": "#/parameters/order" },
{ "$ref": "#/parameters/range" },
{ "$ref": "#/parameters/rangeUnit" },
{ "$ref": "#/parameters/offset" },
{ "$ref": "#/parameters/limit" },
{ "$ref": "#/parameters/preferCount" }
]
|]
describe "RPC" $ do
it "includes function summary/description and body schema for arguments" $ do
r <- simpleBody <$> get "/"
let method s = key "paths" . key "/rpc/varied_arguments" . key s
args = r ^? method "post" . key "parameters" . nth 0 . key "schema"
summary = r ^? method "post" . key "summary"
description = r ^? method "post" . key "description"
liftIO $ do
summary `shouldBe` Just "An RPC function"
description `shouldBe` Just "Just a test for RPC function arguments"
args `shouldBe` Just
[aesonQQ|
{
"required": [
"double",
"varchar",
"boolean",
"date",
"money",
"enum"
],
"properties": {
"double": {
"format": "double precision",
"type": "string"
},
"varchar": {
"format": "character varying",
"type": "string"
},
"boolean": {
"format": "boolean",
"type": "boolean"
},
"date": {
"format": "date",
"type": "string"
},
"money": {
"format": "money",
"type": "string"
},
"enum": {
"format": "enum_menagerie_type",
"type": "string"
},
"integer": {
"format": "integer",
"type": "integer"
}
},
"type": "object",
"description": "An RPC function\n\nJust a test for RPC function arguments"
}
|]
it "doesn't include privileged function for anonymous" $ do
r <- simpleBody <$> get "/"
let funcPath = r ^? key "paths" . key "/rpc/privileged_hello"
liftIO $ funcPath `shouldBe` Nothing
it "includes function if user has permission" $ do
let auth = authHeaderJWT "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.Xod-F15qsGL0WhdOCr2j3DdKuTw9QJERVgoFD3vGaWA"
r <- simpleBody <$> request methodGet "/" [auth] ""
let funcTag = r ^? key "paths" . key "/rpc/privileged_hello"
. key "post" . key "tags"
. nth 0
liftIO $ funcTag `shouldBe` Just [aesonQQ|"(rpc) privileged_hello"|]
it "doesn't include OUT params of function as required parameters" $ do
r <- simpleBody <$> get "/"
let params = r ^? key "paths" . key "/rpc/many_out_params"
. key "post" . key "parameters" . nth 0
. key "schema". key "required"
liftIO $ params `shouldBe` Nothing
it "includes INOUT params(with no DEFAULT) of function as required parameters" $ do
r <- simpleBody <$> get "/"
let params = r ^? key "paths" . key "/rpc/many_inout_params"
. key "post" . key "parameters" . nth 0
. key "schema". key "required"
liftIO $ params `shouldBe` Just [aesonQQ|["num", "str"]|]
describe "Allow header" $ do
it "includes read/write verbs for writeable table" $ do
r <- request methodOptions "/items" [] ""
liftIO $
simpleHeaders r `shouldSatisfy`
matchHeader "Allow" "GET,POST,PATCH,DELETE"
it "includes read verbs for read-only table" $ do
r <- request methodOptions "/has_count_column" [] ""
liftIO $
simpleHeaders r `shouldSatisfy`
matchHeader "Allow" "GET"
+25
View File
@@ -0,0 +1,25 @@
module Feature.UnicodeSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.Wai (Application)
import Control.Monad (void)
import SpecHelper
import Protolude hiding (get)
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 }] |]
{ matchHeaders = [matchContentTypeJson] }
+227
View File
@@ -0,0 +1,227 @@
module Feature.UpsertSpec where
import Test.Hspec
import Test.Hspec.Wai
import Test.Hspec.Wai.JSON
import Network.HTTP.Types
import SpecHelper
import Network.Wai (Application)
import Protolude hiding (get, put)
import Text.Heredoc
spec :: SpecWith Application
spec =
describe "UPSERT" $ do
context "with POST" $ do
context "when Prefer: resolution=merge-duplicates is specified" $ do
it "INSERTs and UPDATEs rows on pk conflict" $
request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json| [
{ "name": "Javascript", "rank": 6 },
{ "name": "Java", "rank": 2 },
{ "name": "C", "rank": 1 }
]|] `shouldRespondWith` [json| [
{ "name": "Javascript", "rank": 6 },
{ "name": "Java", "rank": 2 },
{ "name": "C", "rank": 1 }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
}
it "INSERTs and UPDATEs row on composite pk conflict" $
request methodPost "/employees" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json| [
{ "first_name": "Frances M.", "last_name": "Roe", "salary": "30000" },
{ "first_name": "Peter S.", "last_name": "Yang", "salary": 42000 }
]|] `shouldRespondWith` [json| [
{ "first_name": "Frances M.", "last_name": "Roe", "salary": "$30,000.00", "company": "One-Up Realty", "occupation": "Author" },
{ "first_name": "Peter S.", "last_name": "Yang", "salary": "$42,000.00", "company": null, "occupation": null }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
}
context "when Prefer: resolution=ignore-duplicates is specified" $ do
it "INSERTs and ignores rows on pk conflict" $
request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[
{ "name": "PHP", "rank": 9 },
{ "name": "Python", "rank": 10 }
]|] `shouldRespondWith` [json|[
{ "name": "PHP", "rank": 9 }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
}
it "INSERTs and ignores rows on composite pk conflict" $
request methodPost "/employees" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[
{ "first_name": "Daniel B.", "last_name": "Lyon", "salary": "72000", "company": null, "occupation": null },
{ "first_name": "Sara M.", "last_name": "Torpey", "salary": 60000, "company": "Burstein-Applebee", "occupation": "Soil scientist" }
]|] `shouldRespondWith` [json|[
{ "first_name": "Sara M.", "last_name": "Torpey", "salary": "$60,000.00", "company": "Burstein-Applebee", "occupation": "Soil scientist" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
}
it "succeeds if the table has only PK cols and no other cols" $ do
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "id": 1 }, { "id": 2 }, { "id": 3} ]|]
`shouldRespondWith`
[json|[ { "id": 3} ]|]
{ matchStatus = 201 ,
matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates",
matchContentTypeJson] }
request methodPost "/only_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
`shouldRespondWith`
[json|[ { "id": 1 }, { "id": 2 }, { "id": 4} ]|]
{ matchStatus = 201 ,
matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates",
matchContentTypeJson] }
it "succeeds and ignores the Prefer: resolution header(no Preference-Applied present) if the table has no PK" $
request methodPost "/no_pk" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json|[ { "a": "1", "b": "0" } ]|]
`shouldRespondWith`
[json|[ { "a": "1", "b": "0" } ]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
it "succeeds if not a single resource is created" $ do
request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "name": "Java", "rank": 1 } ]|] `shouldRespondWith`
[json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
request methodPost "/tiobe_pls" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json|[ { "name": "Java", "rank": 1 }, { "name": "C", "rank": 2 } ]|] `shouldRespondWith`
[json|[]|] { matchStatus = 201 , matchHeaders = [matchContentTypeJson] }
context "with PUT" $ do
context "Restrictions" $ do
it "fails if Range is specified" $
request methodPut "/tiobe_pls?name=eq.Javascript" [("Range", "0-5")]
[str| [ { "name": "Javascript", "rank": 1 } ]|] `shouldRespondWith` 400
it "fails if limit is specified" $
put "/tiobe_pls?name=eq.Javascript&limit=1"
[str| [ { "name": "Javascript", "rank": 1 } ]|] `shouldRespondWith` 400
it "fails if offset is specified" $
put "/tiobe_pls?name=eq.Javascript&offset=1"
[str| [ { "name": "Javascript", "rank": 1 } ]|] `shouldRespondWith` 400
it "fails if the payload has more than one row" $
put "/tiobe_pls?name=eq.Go"
[str| [ { "name": "Go", "rank": 19 }, { "name": "Swift", "rank": 12 } ]|] `shouldRespondWith` 400
it "fails if not all columns are specified" $ do
put "/tiobe_pls?name=eq.Go"
[str| [ { "name": "Go" } ]|] `shouldRespondWith` 400
put "/employees?first_name=eq.Susan&last_name=eq.Heidt"
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000" } ]|] `shouldRespondWith` 400
it "rejects every other filter than pk cols eq's" $ do
put "/tiobe_pls?rank=eq.19" [str| [ { "name": "Go", "rank": 19 } ]|] `shouldRespondWith` 405
put "/tiobe_pls?id=not.eq.Java" [str| [ { "name": "Go", "rank": 19 } ]|] `shouldRespondWith` 405
put "/tiobe_pls?id=in.(Go)" [str| [ { "name": "Go", "rank": 19 } ]|] `shouldRespondWith` 405
put "/tiobe_pls?and=(id.eq.Go)" [str| [ { "name": "Go", "rank": 19 } ]|] `shouldRespondWith` 405
it "fails if not all composite key cols are specified as eq filters" $ do
put "/employees?first_name=eq.Susan"
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith` 405
put "/employees?last_name=eq.Heidt"
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith` 405
it "fails if the uri primary key doesn't match the payload primary key" $ do
put "/tiobe_pls?name=eq.MATLAB"
[str| [ { "name": "Perl", "rank": 17 } ]|] `shouldRespondWith` 400
put "/employees?first_name=eq.Wendy&last_name=eq.Anderson"
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|] `shouldRespondWith` 400
it "fails if the table has no PK" $
put "/no_pk?a=eq.one&b=eq.two" [str| [ { "a": "one", "b": "two" } ]|] `shouldRespondWith` 405
context "Inserting row" $ do
it "succeeds on table with single pk col" $ do
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` "[]"
put "/tiobe_pls?name=eq.Go" [str| [ { "name": "Go", "rank": 19 } ]|] `shouldRespondWith` 204
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 19 } ]|] { matchHeaders = [matchContentTypeJson] }
it "succeeds on table with composite pk" $ do
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
`shouldRespondWith` "[]"
put "/employees?first_name=eq.Susan&last_name=eq.Heidt"
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "48000", "company": "GEX", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith` 204
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
`shouldRespondWith`
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
{ matchHeaders = [matchContentTypeJson] }
it "succeeds if the table has only PK cols and no other cols" $ do
get "/only_pk?id=eq.10" `shouldRespondWith` "[]"
put "/only_pk?id=eq.10" [str|[ { "id": 10 } ]|] `shouldRespondWith` 204
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] }
context "Updating row" $ do
it "succeeds on table with single pk col" $ do
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json|[ { "name": "Go", "rank": 19 } ]|] { matchHeaders = [matchContentTypeJson] }
put "/tiobe_pls?name=eq.Go" [str| [ { "name": "Go", "rank": 13 } ]|] `shouldRespondWith` 204
get "/tiobe_pls?name=eq.Go" `shouldRespondWith` [json| [ { "name": "Go", "rank": 13 } ]|] { matchHeaders = [matchContentTypeJson] }
it "succeeds on table with composite pk" $ do
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
`shouldRespondWith`
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$48,000.00", "company": "GEX", "occupation": "Railroad engineer" } ]|]
{ matchHeaders = [matchContentTypeJson] }
put "/employees?first_name=eq.Susan&last_name=eq.Heidt"
[str| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "60000", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
`shouldRespondWith` 204
get "/employees?first_name=eq.Susan&last_name=eq.Heidt"
`shouldRespondWith`
[json| [ { "first_name": "Susan", "last_name": "Heidt", "salary": "$60,000.00", "company": "Gamma Gas", "occupation": "Railroad engineer" } ]|]
{ matchHeaders = [matchContentTypeJson] }
it "succeeds if the table has only PK cols and no other cols" $ do
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] }
put "/only_pk?id=eq.10" [str|[ { "id": 10 } ]|] `shouldRespondWith` 204
get "/only_pk?id=eq.10" `shouldRespondWith` [json|[ { "id": 10 } ]|] { matchHeaders = [matchContentTypeJson] }
it "works with return=representation and vnd.pgrst.object+json" $
request methodPut "/tiobe_pls?name=eq.Ruby"
[("Prefer", "return=representation"), ("Accept", "application/vnd.pgrst.object+json")]
[str| [ { "name": "Ruby", "rank": 11 } ]|]
`shouldRespondWith` [json|{ "name": "Ruby", "rank": 11 }|] { matchHeaders = [matchContentTypeSingular] }
context "with a camel case pk column" $ do
it "works with POST and merge-duplicates/ignore-duplicates headers" $ do
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=merge-duplicates")]
[json| [
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|] `shouldRespondWith` [json|[
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=merge-duplicates", matchContentTypeJson]
}
request methodPost "/UnitTest" [("Prefer", "return=representation"), ("Prefer", "resolution=ignore-duplicates")]
[json| [
{ "idUnitTest": 1, "nameUnitTest": "name of unittest 1" },
{ "idUnitTest": 2, "nameUnitTest": "name of unittest 2" }
]|] `shouldRespondWith` [json|[]|]
{ matchStatus = 201
, matchHeaders = ["Preference-Applied" <:> "resolution=ignore-duplicates", matchContentTypeJson]
}
it "works with PUT" $ do
put "/UnitTest?idUnitTest=eq.1" [str| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] `shouldRespondWith` 204
get "/UnitTest?idUnitTest=eq.1" `shouldRespondWith`
[json| [ { "idUnitTest": 1, "nameUnitTest": "unit test 1" } ]|] { matchHeaders = [matchContentTypeJson] }
+132 -2
View File
@@ -2,7 +2,137 @@ module Main where
import Test.Hspec
import SpecHelper
import Spec
import qualified Hasql.Pool as P
import qualified Hasql.Transaction.Sessions as HT
import PostgREST.App (postgrest)
import PostgREST.DbStructure (getDbStructure, getPgVersion)
import PostgREST.Types (DbStructure(..), pgVersion95, pgVersion96)
import Control.AutoUpdate (defaultUpdateSettings, mkAutoUpdate, updateAction)
import Data.Function (id)
import Data.IORef
import Data.Time.Clock (getCurrentTime)
import qualified Feature.AuthSpec
import qualified Feature.AsymmetricJwtSpec
import qualified Feature.BinaryJwtSecretSpec
import qualified Feature.AudienceJwtSecretSpec
import qualified Feature.ConcurrentSpec
import qualified Feature.CorsSpec
import qualified Feature.DeleteSpec
import qualified Feature.ExtraSearchPathSpec
import qualified Feature.InsertSpec
import qualified Feature.JsonOperatorSpec
import qualified Feature.NoJwtSpec
import qualified Feature.QueryLimitedSpec
import qualified Feature.QuerySpec
import qualified Feature.RangeSpec
import qualified Feature.StructureSpec
import qualified Feature.SingularSpec
import qualified Feature.UnicodeSpec
import qualified Feature.ProxySpec
import qualified Feature.AndOrParamsSpec
import qualified Feature.RpcSpec
import qualified Feature.NonexistentSchemaSpec
import qualified Feature.PgVersion95Spec
import qualified Feature.PgVersion96Spec
import qualified Feature.UpsertSpec
import Protolude
main :: IO ()
main = resetDb >> hspec spec
main = do
testDbConn <- getEnvVarWithDefault "POSTGREST_TEST_CONNECTION" "postgres://postgrest_test@localhost/postgrest_test"
setupDb testDbConn
pool <- P.acquire (3, 10, toS testDbConn)
result <- P.use pool $ do
ver <- getPgVersion
HT.transaction HT.ReadCommitted HT.Read $ getDbStructure "test" ver
dbStructure <- pure $ either (panic.show) id result
getTime <- mkAutoUpdate defaultUpdateSettings { updateAction = getCurrentTime }
refDbStructure <- newIORef $ Just dbStructure
let withApp = return $ postgrest (testCfg testDbConn) refDbStructure pool getTime $ pure ()
ltdApp = return $ postgrest (testLtdRowsCfg testDbConn) refDbStructure pool getTime $ pure ()
unicodeApp = return $ postgrest (testUnicodeCfg testDbConn) refDbStructure pool getTime $ pure ()
proxyApp = return $ postgrest (testProxyCfg testDbConn) refDbStructure pool getTime $ pure ()
noJwtApp = return $ postgrest (testCfgNoJWT testDbConn) refDbStructure pool getTime $ pure ()
binaryJwtApp = return $ postgrest (testCfgBinaryJWT testDbConn) refDbStructure pool getTime $ pure ()
audJwtApp = return $ postgrest (testCfgAudienceJWT testDbConn) refDbStructure pool getTime $ pure ()
asymJwkApp = return $ postgrest (testCfgAsymJWK testDbConn) refDbStructure pool getTime $ pure ()
asymJwkSetApp = return $ postgrest (testCfgAsymJWKSet testDbConn) refDbStructure pool getTime $ pure ()
nonexistentSchemaApp = return $ postgrest (testNonexistentSchemaCfg testDbConn) refDbStructure pool getTime $ pure ()
extraSearchPathApp = return $ postgrest (testCfgExtraSearchPath testDbConn) refDbStructure pool getTime $ pure ()
let reset :: IO ()
reset = resetDb testDbConn
actualPgVersion = pgVersion dbStructure
extraSpecs =
[("Feature.UpsertSpec", Feature.UpsertSpec.spec) | actualPgVersion >= pgVersion95] ++
[("Feature.PgVersion95Spec", Feature.PgVersion95Spec.spec) | actualPgVersion >= pgVersion95] ++
[("Feature.PgVersion96Spec", Feature.PgVersion96Spec.spec) | actualPgVersion >= pgVersion96]
specs = 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.JsonOperatorSpec" , Feature.JsonOperatorSpec.spec)
, ("Feature.QuerySpec" , Feature.QuerySpec.spec)
, ("Feature.RpcSpec" , Feature.RpcSpec.spec)
, ("Feature.RangeSpec" , Feature.RangeSpec.spec)
, ("Feature.SingularSpec" , Feature.SingularSpec.spec)
, ("Feature.StructureSpec" , Feature.StructureSpec.spec)
, ("Feature.AndOrParamsSpec" , Feature.AndOrParamsSpec.spec)
] ++ extraSpecs
hspec $ do
mapM_ (beforeAll_ reset . before withApp) specs
-- this test runs with a different server flag
beforeAll_ reset . before ltdApp $
describe "Feature.QueryLimitedSpec" Feature.QueryLimitedSpec.spec
-- this test runs with a different schema
beforeAll_ reset . before unicodeApp $
describe "Feature.UnicodeSpec" Feature.UnicodeSpec.spec
-- this test runs with a proxy
beforeAll_ reset . before proxyApp $
describe "Feature.ProxySpec" Feature.ProxySpec.spec
-- this test runs without a JWT secret
beforeAll_ reset . before noJwtApp $
describe "Feature.NoJwtSpec" Feature.NoJwtSpec.spec
-- this test runs with a binary JWT secret
beforeAll_ reset . before binaryJwtApp $
describe "Feature.BinaryJwtSecretSpec" Feature.BinaryJwtSecretSpec.spec
-- this test runs with a binary JWT secret and an audience claim
beforeAll_ reset . before audJwtApp $
describe "Feature.AudienceJwtSecretSpec" Feature.AudienceJwtSecretSpec.spec
-- this test runs with asymmetric JWK
beforeAll_ reset . before asymJwkApp $
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
-- this test runs with asymmetric JWKSet
beforeAll_ reset . before asymJwkSetApp $
describe "Feature.AsymmetricJwtSpec" Feature.AsymmetricJwtSpec.spec
-- this test runs with a nonexistent db-schema
beforeAll_ reset . before nonexistentSchemaApp $
describe "Feature.NonexistentSchemaSpec" Feature.NonexistentSchemaSpec.spec
-- this test runs with an extra search path
beforeAll_ reset . before extraSearchPathApp $
describe "Feature.ExtraSearchPathSpec" Feature.ExtraSearchPathSpec.spec
-1
View File
@@ -1 +0,0 @@
{-# OPTIONS_GHC -F -pgmF hspec-discover -optF --no-main #-}
+141 -115
View File
@@ -1,154 +1,180 @@
module SpecHelper where
import Network.Wai
import Control.Monad (void)
import qualified System.IO.Error as E
import System.Environment (getEnv)
import qualified Data.ByteString.Base64 as B64 (encode, decodeLenient)
import Data.CaseInsensitive (CI(..))
import qualified Data.Set as S
import qualified Data.Map.Strict as M
import Data.List (lookup)
import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS
import qualified Data.ByteString.Lazy as BL
import System.Process (readProcess)
import Text.Heredoc
import PostgREST.Config (AppConfig(..))
import PostgREST.Types (JSPathExp(..))
import Test.Hspec
import Test.Hspec.Wai
import Hasql as H
import Hasql.Backend as B
import Hasql.Postgres as P
import Network.HTTP.Types
import Network.Wai.Test (SResponse(simpleStatus, simpleHeaders, simpleBody))
import Data.String.Conversions (cs)
import Data.Monoid
import Data.Text hiding (map)
import qualified Data.Vector as V
import Control.Monad (void)
import Data.Maybe (fromJust)
import Data.Aeson (decode, Value(..))
import qualified JSONSchema.Draft4 as D4
import Network.HTTP.Types.Header (Header, ByteRange, renderByteRange,
hRange, hAuthorization, hAccept)
import Codec.Binary.Base64.String (encode)
import Data.CaseInsensitive (CI(..))
import Data.Maybe (fromMaybe)
import Text.Regex.TDFA ((=~))
import qualified Data.ByteString.Char8 as BS
import System.Process (readProcess)
import Protolude
import qualified Data.Aeson.Types as J
matchContentTypeJson :: MatchHeader
matchContentTypeJson = "Content-Type" <:> "application/json; charset=utf-8"
import PostgREST.App (app)
import PostgREST.Config (AppConfig(..))
import PostgREST.Middleware
import PostgREST.Error(errResponse)
matchContentTypeSingular :: MatchHeader
matchContentTypeSingular = "Content-Type" <:> "application/vnd.pgrst.object+json; charset=utf-8"
isLeft :: Either a b -> Bool
isLeft (Left _ ) = True
isLeft _ = False
validateOpenApiResponse :: [Header] -> WaiSession ()
validateOpenApiResponse headers = do
r <- request methodGet "/" headers ""
liftIO $
let respStatus = simpleStatus r in
respStatus `shouldSatisfy`
\s -> s == Status { statusCode = 200, statusMessage="OK" }
liftIO $
let respHeaders = simpleHeaders r in
respHeaders `shouldSatisfy`
\hs -> ("Content-Type", "application/openapi+json; charset=utf-8") `elem` hs
liftIO $
let respBody = simpleBody r
schema :: D4.Schema
schema = D4.emptySchema { D4._schemaRef = Just "openapi.json" }
schemaContext :: D4.SchemaWithURI D4.Schema
schemaContext = D4.SchemaWithURI
{ D4._swSchema = schema
, D4._swURI = Just "test/fixtures/openapi.json"
}
in
D4.fetchFilesystemAndValidate schemaContext ((fromJust . decode) respBody) `shouldReturn` Right ()
cfg :: AppConfig
cfg = AppConfig "postgrest_test" 5432 "postgrest_test" "" "localhost" 3000 "postgrest_anonymous" False 10 "1" "safe"
getEnvVarWithDefault :: Text -> Text -> IO Text
getEnvVarWithDefault var def = toS <$>
getEnv (toS var) `E.catchIOError` const (return $ toS def)
testPoolOpts :: PoolSettings
testPoolOpts = fromMaybe (error "bad settings") $ H.poolSettings 1 30
_baseCfg :: AppConfig
_baseCfg = -- Connection Settings
AppConfig mempty "postgrest_test_anonymous" Nothing "test" "localhost" 3000
-- Jwt settings
(Just $ encodeUtf8 "reallyreallyreallyreallyverysafe") False Nothing
-- Connection Modifiers
10 Nothing (Just "test.switch_role")
-- Debug Settings
True
[ ("app.settings.app_host", "localhost")
, ("app.settings.external_api_secret", "0123456789abcdef")
]
-- Default role claim key
(Right [JSPKey "role"])
-- Empty db-extra-search-path
[]
pgSettings :: P.Settings
pgSettings = P.ParamSettings (cs $ configDbHost cfg)
(fromIntegral $ configDbPort cfg)
(cs $ configDbUser cfg)
(cs $ configDbPass cfg)
(cs $ configDbName cfg)
testCfg :: Text -> AppConfig
testCfg testDbConn = _baseCfg { configDatabase = testDbConn }
withApp :: ActionWith Application -> IO ()
withApp perform = do
pool :: H.Pool P.Postgres
<- H.acquirePool pgSettings testPoolOpts
testCfgNoJWT :: Text -> AppConfig
testCfgNoJWT testDbConn = (testCfg testDbConn) { configJwtSecret = Nothing }
perform $ middle $ \req resp -> do
body <- strictRequestBody req
result <- liftIO $ H.session pool $ H.tx (Just (H.ReadCommitted, Just True))
$ authenticated cfg (app cfg body) req
either (resp . errResponse) resp result
testUnicodeCfg :: Text -> AppConfig
testUnicodeCfg testDbConn = (testCfg testDbConn) { configSchema = "تست" }
where middle = defaultMiddle False
testLtdRowsCfg :: Text -> AppConfig
testLtdRowsCfg testDbConn = (testCfg testDbConn) { configMaxRows = Just 2 }
testProxyCfg :: Text -> AppConfig
testProxyCfg testDbConn = (testCfg testDbConn) { configProxyUri = Just "https://postgrest.com/openapi.json" }
resetDb :: IO ()
resetDb = do
pool :: H.Pool P.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 |]
testCfgBinaryJWT :: Text -> AppConfig
testCfgBinaryJWT testDbConn = (testCfg testDbConn) {
configJwtSecret = Just . B64.decodeLenient $
"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU="
}
loadFixture "roles"
loadFixture "schema"
testCfgAudienceJWT :: Text -> AppConfig
testCfgAudienceJWT testDbConn = (testCfg testDbConn) {
configJwtSecret = Just . B64.decodeLenient $
"cmVhbGx5cmVhbGx5cmVhbGx5cmVhbGx5dmVyeXNhZmU=",
configJwtAudience = Just "youraudience"
}
testCfgAsymJWK :: Text -> AppConfig
testCfgAsymJWK testDbConn = (testCfg testDbConn) {
configJwtSecret = Just $ encodeUtf8
[str|{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}|]
}
loadFixture :: FilePath -> IO()
loadFixture name =
void $ readProcess "psql" ["-U", "postgrest_test", "-d", "postgrest_test", "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
testCfgAsymJWKSet :: Text -> AppConfig
testCfgAsymJWKSet testDbConn = (testCfg testDbConn) {
configJwtSecret = Just $ encodeUtf8
[str|{"keys": [{"alg":"RS256","e":"AQAB","key_ops":["verify"],"kty":"RSA","n":"0etQ2Tg187jb04MWfpuogYGV75IFrQQBxQaGH75eq_FpbkyoLcEpRUEWSbECP2eeFya2yZ9vIO5ScD-lPmovePk4Aa4SzZ8jdjhmAbNykleRPCxMg0481kz6PQhnHRUv3nF5WP479CnObJKqTVdEagVL66oxnX9VhZG9IZA7k0Th5PfKQwrKGyUeTGczpOjaPqbxlunP73j9AfnAt4XCS8epa-n3WGz1j-wfpr_ys57Aq-zBCfqP67UYzNpeI1AoXsJhD9xSDOzvJgFRvc3vm2wjAW4LEMwi48rCplamOpZToIHEPIaPzpveYQwDnB1HFTR1ove9bpKJsHmi-e2uzQ","use":"sig"}]}|]
}
testNonexistentSchemaCfg :: Text -> AppConfig
testNonexistentSchemaCfg testDbConn = (testCfg testDbConn) { configSchema = "nonexistent" }
testCfgExtraSearchPath :: Text -> AppConfig
testCfgExtraSearchPath testDbConn = (testCfg testDbConn) { configExtraSearchPath = ["public", "extensions"] }
setupDb :: Text -> IO ()
setupDb dbConn = do
loadFixture dbConn "database"
loadFixture dbConn "roles"
loadFixture dbConn "schema"
loadFixture dbConn "jwt"
loadFixture dbConn "privileges"
resetDb dbConn
resetDb :: Text -> IO ()
resetDb dbConn = loadFixture dbConn "data"
loadFixture :: Text -> FilePath -> IO()
loadFixture dbConn name =
void $ readProcess "psql" [toS dbConn, "-a", "-f", "test/fixtures/" ++ name ++ ".sql"] []
rangeHdrs :: ByteRange -> [Header]
rangeHdrs r = [rangeUnit, (hRange, renderByteRange r)]
rangeHdrsWithCount :: ByteRange -> [Header]
rangeHdrsWithCount r = ("Prefer", "count=exact") : rangeHdrs r
acceptHdrs :: BS.ByteString -> [Header]
acceptHdrs mime = [(hAccept, mime)]
rangeUnit :: Header
rangeUnit = ("Range-Unit" :: CI BS.ByteString, "items")
matchHeader :: CI BS.ByteString -> String -> [Header] -> Bool
matchHeader :: CI BS.ByteString -> BS.ByteString -> [Header] -> Bool
matchHeader name valRegex headers =
maybe False (=~ valRegex) $ lookup name headers
authHeaderBasic :: String -> String -> Header
authHeaderBasic :: BS.ByteString -> BS.ByteString -> Header
authHeaderBasic u p =
(hAuthorization, cs $ "Basic " ++ encode (u ++ ":" ++ p))
(hAuthorization, "Basic " <> (toS . B64.encode . toS $ u <> ":" <> p))
authHeaderJWT :: String -> Header
authHeaderJWT :: BS.ByteString -> Header
authHeaderJWT token =
(hAuthorization, cs $ "Bearer " ++ token)
(hAuthorization, "Bearer " <> token)
testPool :: IO(H.Pool P.Postgres)
testPool = H.acquirePool pgSettings testPoolOpts
clearTable :: Text -> IO ()
clearTable table = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ B.Stmt ("delete from \"1\"."<>table) V.empty True
createItems :: Int -> IO ()
createItems n = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = mapM_ H.unitEx stmts
stmts = map [H.stmt|insert into "1".items (id) values (?)|] [1..n]
createNulls :: Int -> IO ()
createNulls n = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing txn
where
txn = mapM_ H.unitEx (stmt':stmts)
stmt' = [H.stmt|insert into "1".no_pk (a,b) values (null,null)|]
stmts = map [H.stmt|insert into "1".no_pk (a,b) values (?,0)|] [1..n]
createNullInteger :: IO ()
createNullInteger = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $ [H.stmt| insert into "1".nullable_integer (a) values (null) |]
createLikableStrings :: IO ()
createLikableStrings = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $ do
H.unitEx $ insertSimplePk "xyyx" "u"
H.unitEx $ insertSimplePk "xYYx" "v"
where
insertSimplePk :: Text -> Text -> H.Stmt P.Postgres
insertSimplePk = [H.stmt|insert into "1".simple_pk (k, extra) values (?,?)|]
createJsonData :: IO ()
createJsonData = do
pool <- testPool
void . liftIO $ H.session pool $ H.tx Nothing $
H.unitEx $
[H.stmt|
insert into "1".json (data) values (?)
|]
(J.object [("foo", J.object [("bar", J.String "baz")])])
-- | Tests whether the text can be parsed as a json object comtaining
-- the key "message", and optional keys "details", "hint", "code",
-- and no extraneous keys
isErrorFormat :: BL.ByteString -> Bool
isErrorFormat s =
"message" `S.member` keys &&
S.null (S.difference keys validKeys)
where
obj = decode s :: Maybe (M.Map Text Value)
keys = maybe S.empty M.keysSet obj
validKeys = S.fromList ["message", "details", "hint", "code"]
+6 -24
View File
@@ -1,23 +1,18 @@
module TestTypes (
IncPK(..)
, CompoundPK(..)
-- , incFromList
-- , compoundFromList
) where
import qualified Data.Aeson as JSON
import Data.Aeson ((.:))
-- import Data.Maybe (fromJust)
import Control.Applicative
import Control.Monad (mzero)
import Prelude
import Protolude
data IncPK = IncPK {
incId :: Int
, incNullableStr :: Maybe String
, incStr :: String
, incInsert :: String
, incNullableStr :: Maybe Text
, incStr :: Text
, incInsert :: Text
} deriving (Eq, Show)
instance JSON.FromJSON IncPK where
@@ -28,18 +23,11 @@ instance JSON.FromJSON IncPK where
r .: "inserted_at"
parseJSON _ = mzero
-- incFromList :: [(String, SqlValue)] -> IncPK
-- incFromList row = IncPK
-- (fromSql . fromJust $ lookup "id" row)
-- (fromSql . fromJust $ lookup "nullable_string" row)
-- (fromSql . fromJust $ lookup "non_nullable_string" row)
-- (fromSql . fromJust $ lookup "inserted_at" row)
data CompoundPK = CompoundPK {
compoundK1 :: Int
, compoundK2 :: Int
, compoundK2 :: Text
, compoundExtra :: Maybe Int
}
} deriving (Eq, Show)
instance JSON.FromJSON CompoundPK where
parseJSON (JSON.Object r) = CompoundPK <$>
@@ -47,9 +35,3 @@ instance JSON.FromJSON CompoundPK where
r .: "k2" <*>
r .: "extra"
parseJSON _ = mzero
-- compoundFromList :: [(String, SqlValue)] -> CompoundPK
-- compoundFromList row = CompoundPK
-- (fromSql . fromJust $ lookup "k1" row)
-- (fromSql . fromJust $ lookup "k2" row)
-- (fromSql . fromJust $ lookup "extra" row)
@@ -1,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","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"})]
+6 -6
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
+60
View File
@@ -0,0 +1,60 @@
#! /usr/bin/env bash
if [ -z "$1" ]
then
echo "Please supply the connection uri for the user with create database privileges"
exit -1
fi
if [ -z "$2" ]
then
echo "Please supply the test database name"
exit -1
fi
if [[ $1 != postgres://* ]]
then
echo "Please use a valid connection URI (https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347)"
exit -1
fi
BASEPATH=$( cd $(dirname $0) ; pwd -P )
URI="$1"
#Extract host and port--we need this to form the new connection string
HOST_PORT=$(echo $URI | cut -d'/' -f3 | cut -d'@' -f2 )
DB=$2
# Specify the username of choice, or let the script create a random unique user by appending the database name
TEST_USER_NAME=postgrest_test_authenticator
# New password will get assigned only if the user does not already exist
# Otherwise make sure to provide the correct password for the existing user
TEST_USER_PASS=$(cat /dev/urandom | env LC_CTYPE=C tr -dc 'a-zA-Z0-9' | fold -w 16 | head -n 1)
PGOPTIONS='-c client_min_messages=WARNING' psql "$URI" -Xq >/dev/null -c 'select rolcreatedb from pg_authid where rolname = current_user;' 2>/dev/null
if [ $? -ne 0 ]; then
echo "ERROR: Please specify the user with 'Create DB' permissions, and ensure that the default database for the username exists."
exit 1
fi
# plpgsql does not like psql variables, easier to pull this part off with bash variables
PGOPTIONS='-c client_min_messages=WARNING' psql "$URI" -Xq >/dev/null <<EOF
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE pg_stat_activity.datname = '$DB'
AND pid <> pg_backend_pid();
DROP DATABASE IF EXISTS $DB;
DROP ROLE IF EXISTS $TEST_USER_NAME;
CREATE USER $TEST_USER_NAME WITH SUPERUSER LOGIN NOINHERIT PASSWORD '$TEST_USER_PASS' CREATEROLE;
CREATE DATABASE $DB OWNER $TEST_USER_NAME;
\\connect $DB
ALTER SCHEMA public OWNER TO $TEST_USER_NAME;
EOF
#Remove database path from the connection uri--prevents setting up the new database name with PGDATABASE
URI=$(echo $URI | cut -d'/' -f1-3)
PGDATABASE=$DB PGOPTIONS='-c client_min_messages=WARNING' psql "$URI" --set=db=$DB -Xq <<EOF
CREATE EXTENSION IF NOT EXISTS pgcrypto;
ALTER DATABASE ${DB} SET request.jwt.claim.id = '-1';
EOF
# Create a new connection string to use with the test runner
echo 'postgres://'${TEST_USER_NAME}':'$TEST_USER_PASS'@'$HOST_PORT'/'$DB
+65
View File
@@ -0,0 +1,65 @@
#! /usr/bin/env bash
if [ -z "$1" ]
then
echo "Please supply the connection uri for the user with create database privileges"
exit -1
fi
if [ -z "$2" ]
then
echo "Please supply the test database name"
exit -1
fi
if [[ $1 != postgres://* ]]
then
echo "Please use a valid connection URI (https://www.postgresql.org/docs/current/static/libpq-connect.html#AEN45347)"
exit -1
fi
BASEPATH=$( cd $(dirname $0) ; pwd -P )
URI="$1"
DB=$2
PGOPTIONS='-c client_min_messages=WARNING' psql "$URI" -Xq >/dev/null -c 'select rolcreatedb from pg_authid where rolname = current_user;' 2>/dev/null
if [ $? -ne 0 ]; then
echo "ERROR: Please specify the user with 'Create DB' permissions, and ensure that the default database for the username exists."
exit 1
fi
# plpgsql does not like psql variables, easier to pull this part off with bash variables
PGOPTIONS='-c client_min_messages=WARNING' psql "$URI" -Xq >/dev/null <<EOF
SELECT pg_terminate_backend(pg_stat_activity.pid)
FROM pg_stat_activity
WHERE pg_stat_activity.datname = '$DB'
AND pid <> pg_backend_pid();
drop database if exists "$DB";
-- Find all test users that were members of role 'postgrest_test_author'--that way we don't have to know the
-- test user name (in case it was auto-generated).
DO \$\$
DECLARE
mem text;
BEGIN
FOR mem IN
SELECT pg_get_userbyid(member)
FROM pg_roles r
JOIN pg_auth_members m
ON m.roleid = r.oid
WHERE rolname = 'postgrest_test_author'
LOOP
EXECUTE 'drop role '|| mem || ';';
END LOOP;
END \$\$;
DO \$\$
DECLARE
r text;
BEGIN
FOR r IN
VALUES('postgrest_test_author'),('postgrest_test_anonymous'),('postgrest_test_default_role')
LOOP
EXECUTE 'drop role if exists '|| r || ';';
END LOOP;
END \$\$;
EOF
+491
View File
@@ -0,0 +1,491 @@
--
-- 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', 15, true);
--
-- Data for Name: json; Type: TABLE DATA; Schema: test; Owner: -
--
TRUNCATE TABLE json CASCADE;
INSERT INTO json VALUES ('{"foo":{"bar":"baz"},"id":1}');
INSERT INTO json VALUES ('{"id":3}');
INSERT INTO json VALUES ('{"id":0}');
--
-- 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 (to_tsvector('It''s kind of fun to do the impossible'));
INSERT INTO tsearch VALUES (to_tsvector('But also fun to do what is possible'));
INSERT INTO tsearch VALUES (to_tsvector('Fat cats ate rats'));
INSERT INTO tsearch VALUES (to_tsvector('french', 'C''est un peu amusant de faire l''impossible'));
INSERT INTO tsearch VALUES (to_tsvector('german', 'Es ist eine Art Spaß, das Unmögliche zu machen'));
--
-- 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');
TRUNCATE TABLE addresses CASCADE;
INSERT INTO addresses VALUES (1, 'address 1');
INSERT INTO addresses VALUES (2, 'address 2');
INSERT INTO addresses VALUES (3, 'address 3');
INSERT INTO addresses VALUES (4, 'address 4');
TRUNCATE TABLE orders CASCADE;
INSERT INTO orders VALUES (1, 'order 1', 1, 2);
INSERT INTO orders VALUES (2, 'order 2', 3, 4);
TRUNCATE TABLE images CASCADE;
INSERT INTO images(name, img) VALUES ('A.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEUAAAD/AAAb/40iAAAAP0lEQVQI12NgwAbYG2AE/wEYwQMiZB4ACQkQYZEAIgqAhAGIKLCAEQ8kgMT/P1CCEUwc4IMSzA3sUIIdCHECAGSQEkeOTUyCAAAAAElFTkSuQmCC', 'base64'));
INSERT INTO images(name, img) VALUES ('B.png', decode('iVBORw0KGgoAAAANSUhEUgAAAB4AAAAeAQMAAAAB/jzhAAAABlBMVEX///8AAP94wDzzAAAAL0lEQVQIW2NgwAb+HwARH0DEDyDxwAZEyGAhLODqHmBRzAcn5GAS///A1IF14AAA5/Adbiiz/0gAAAAASUVORK5CYII=', 'base64'));
TRUNCATE TABLE w_or_wo_comma_names CASCADE;
INSERT INTO w_or_wo_comma_names VALUES ('Hebdon, John');
INSERT INTO w_or_wo_comma_names VALUES ('Williams, Mary');
INSERT INTO w_or_wo_comma_names VALUES ('Smith, Joseph');
INSERT INTO w_or_wo_comma_names VALUES ('David White');
INSERT INTO w_or_wo_comma_names VALUES ('Larry Thompson');
TRUNCATE TABLE items_with_different_col_types CASCADE;
INSERT INTO items_with_different_col_types VALUES (1, null, null, null, null, null, null, null);
TRUNCATE TABLE entities CASCADE;
INSERT INTO entities VALUES (1, 'entity 1', '{1}', '''bar'':2 ''foo'':1');
INSERT INTO entities VALUES (2, 'entity 2', '{1,2}', '''baz'':1 ''qux'':2');
INSERT INTO entities VALUES (3, 'entity 3', '{1,2,3}', null);
INSERT INTO entities VALUES (4, null, null, null);
TRUNCATE TABLE child_entities CASCADE;
INSERT INTO child_entities VALUES (1, 'child entity 1', 1);
INSERT INTO child_entities VALUES (2, 'child entity 2', 1);
INSERT INTO child_entities VALUES (3, 'child entity 3', 2);
TRUNCATE TABLE grandchild_entities CASCADE;
INSERT INTO grandchild_entities VALUES (1, 'grandchild entity 1', 1, null, null, null);
INSERT INTO grandchild_entities VALUES (2, 'grandchild entity 2', 1, null, null, null);
INSERT INTO grandchild_entities VALUES (3, 'grandchild entity 3', 2, null, null, null);
INSERT INTO grandchild_entities VALUES (4, '(grandchild,entity,4)', 2, null, null, '{"a": {"b":"foo"}}');
INSERT INTO grandchild_entities VALUES (5, '(grandchild,entity,5)', 2, null, null, '{"b":"bar"}');
TRUNCATE TABLE ranges CASCADE;
INSERT INTO ranges VALUES (1, '[1,3]');
INSERT INTO ranges VALUES (2, '[3,6]');
INSERT INTO ranges VALUES (3, '[6,9]');
INSERT INTO ranges VALUES (4, '[9,12]');
TRUNCATE TABLE being CASCADE;
INSERT INTO being VALUES (1), (2), (3), (4);
TRUNCATE TABLE descendant CASCADE;
INSERT INTO descendant VALUES (1,1), (2,1), (3,1), (4,2);
TRUNCATE TABLE part CASCADE;
INSERT INTO part VALUES (1), (2), (3), (4);
TRUNCATE TABLE being_part CASCADE;
INSERT INTO being_part VALUES (1,1), (2,1), (3,2), (4,3);
TRUNCATE TABLE employees CASCADE;
INSERT INTO employees VALUES
('Frances M.', 'Roe', '24000', 'One-Up Realty', 'Author'),
('Daniel B.', 'Lyon', '36000', 'Dubrow''s Cafeteria', 'Packer'),
('Edwin S.', 'Smith', '48000', 'Pro Garden Management', 'Marine biologist');
TRUNCATE TABLE tiobe_pls CASCADE;
INSERT INTO tiobe_pls VALUES ('Java', 1), ('C', 2), ('Python', 4);
TRUNCATE TABLE only_pk CASCADE;
INSERT INTO only_pk VALUES (1), (2);
TRUNCATE TABLE family_tree CASCADE;
INSERT INTO family_tree VALUES ('1', 'Parental Unit', NULL);
INSERT INTO family_tree VALUES ('2', 'Kid One', '1');
INSERT INTO family_tree VALUES ('3', 'Kid Two', '1');
INSERT INTO family_tree VALUES ('4', 'Grandkid One', '2');
INSERT INTO family_tree VALUES ('5', 'Grandkid Two', '3');
TRUNCATE TABLE organizations CASCADE;
INSERT INTO organizations VALUES (1, 'Referee Org', null, null);
INSERT INTO organizations VALUES (2, 'Auditor Org', null, null);
INSERT INTO organizations VALUES (3, 'Acme', 1, 2);
INSERT INTO organizations VALUES (4, 'Umbrella', 1, 2);
SET search_path = private, pg_catalog;
TRUNCATE TABLE authors CASCADE;
INSERT INTO authors VALUES (1, 'George Orwell');
INSERT INTO authors VALUES (2, 'Anne Frank');
INSERT INTO authors VALUES (3, 'Antoine de Saint-Exupéry');
INSERT INTO authors VALUES (4, 'J.D. Salinger');
INSERT INTO authors VALUES (5, 'Ray Bradbury');
INSERT INTO authors VALUES (6, 'William Golding');
INSERT INTO authors VALUES (7, 'Harper Lee');
INSERT INTO authors VALUES (8, 'Kurt Vonnegut');
INSERT INTO authors VALUES (9, 'Ken Kesey');
TRUNCATE TABLE publishers CASCADE;
INSERT INTO publishers VALUES (1, 'Secker & Warburg');
INSERT INTO publishers VALUES (2, 'Contact Publishing');
INSERT INTO publishers VALUES (3, 'Reynal & Hitchcock');
INSERT INTO publishers VALUES (4, 'Little, Brown and Company');
INSERT INTO publishers VALUES (5, 'Ballantine Books');
INSERT INTO publishers VALUES (6, 'Faber and Faber');
INSERT INTO publishers VALUES (7, 'J. B. Lippincott & Co.');
INSERT INTO publishers VALUES (8, 'Delacorte');
INSERT INTO publishers VALUES (9, 'Viking Press & Signet Books');
TRUNCATE TABLE books CASCADE;
INSERT INTO books VALUES (1, '1984', 1949, 1, 1);
INSERT INTO books VALUES (2, 'The Diary of a Young Girl', 1947, 2, 2);
INSERT INTO books VALUES (3, 'The Little Prince', 1947, 3, 3);
INSERT INTO books VALUES (4, 'The Catcher in the Rye', 1951, 4, 4);
INSERT INTO books VALUES (5, 'Farenheit 451', 1953, 5, 5);
INSERT INTO books VALUES (6, 'Lord of the Flies', 1954, 6, 6);
INSERT INTO books VALUES (7, 'To Kill a Mockingbird', 1960, 7, 7);
INSERT INTO books VALUES (8, 'Slaughterhouse-Five', 1969, 8, 8);
INSERT INTO books VALUES (9, 'One Flew Over the Cuckoo''s Nest', 1962, 9, 9);
SET search_path = test, pg_catalog;
TRUNCATE TABLE person CASCADE;
INSERT INTO person VALUES (1, 'John');
INSERT INTO person VALUES (2, 'Jane');
INSERT INTO person VALUES (3, 'Jake');
INSERT INTO person VALUES (4, 'Julie');
TRUNCATE TABLE message CASCADE;
INSERT INTO message VALUES (1, 'Hello Jane', 1, 2);
INSERT INTO message VALUES (2, 'Hi John', 2, 1);
INSERT INTO message VALUES (3, 'How are you doing?', 1, 2);
INSERT INTO message VALUES (4, 'Hey Julie', 3, 4);
INSERT INTO message VALUES (5, 'What''s up Jake', 4, 3);
TRUNCATE TABLE space CASCADE;
INSERT INTO space VALUES (1, 'space 1');
TRUNCATE TABLE zone CASCADE;
INSERT INTO zone VALUES (1, 'zone 1', 2, 1);
INSERT INTO zone VALUES (2, 'zone 2', 2, 1);
INSERT INTO zone VALUES (3, 'store 3', 3, 1);
INSERT INTO zone VALUES (4, 'store 4', 3, 1);
-- for foreign table projects_dump
copy (select id, name, client_id from projects) to '/tmp/projects_dump.csv' with csv;
TRUNCATE TABLE "UnitTest" CASCADE;
INSERT INTO "UnitTest" VALUES (1, 'unit test 1');
TRUNCATE TABLE json_arr CASCADE;
INSERT INTO json_arr VALUES (1, '[1, 2, 3]');
INSERT INTO json_arr VALUES (2, '[4, 5, 6]');
INSERT INTO json_arr VALUES (3, '[[9, 8, 7], [11, 12, 13]]');
INSERT INTO json_arr VALUES (4, '[[[5, 6], 7, 8]]');
INSERT INTO json_arr VALUES (5, '[{"a": "A"}, {"b": "B"}]');
INSERT INTO json_arr VALUES (6, '[{"a": [1,2,3]}, {"b": [4,5]}]');
INSERT INTO json_arr VALUES (7, '{"c": [1,2,3], "d": [4,5]}');
INSERT INTO json_arr VALUES (8, '{"c": [{"d": [4,5,6,7,8]}]}');
INSERT INTO json_arr VALUES (9, '[{"0xy1": [1,{"23-xy-45": [2, {"xy-6": [3]}]}]}]');
TRUNCATE TABLE jsonb_test CASCADE;
INSERT INTO jsonb_test VALUES (1, '{ "a": {"b": 2} }');
INSERT INTO jsonb_test VALUES (2, '{ "c": [1,2,3] }');
INSERT INTO jsonb_test VALUES (3, '[{ "d": "test" }]');
INSERT INTO jsonb_test VALUES (4, '{ "e": 1 }');
TRUNCATE TABLE private.player CASCADE;
INSERT into private.player
SELECT
generate_series,
'first_name_' || generate_series,
'last_name_' || generate_series,
'2018-10-11'
FROM generate_series(1, 12);
TRUNCATE TABLE contract CASCADE;
insert into contract
select
'tournament_' || generate_series,
tsrange(now()::timestamp, null),
10*generate_series,
generate_series,
'first_name_' || generate_series,
'last_name_' || generate_series,
'2018-10-11'
from generate_series(1, 6);
TRUNCATE TABLE ltree_sample CASCADE;
INSERT INTO ltree_sample VALUES ('Top');
INSERT INTO ltree_sample VALUES ('Top.Science');
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy');
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Astrophysics');
INSERT INTO ltree_sample VALUES ('Top.Science.Astronomy.Cosmology');
TRUNCATE TABLE isn_sample CASCADE;
INSERT INTO isn_sample VALUES ('978-0-393-04002-9', 'Mathematics: From the Birth of Numbers');
TRUNCATE TABLE "Server Today" CASCADE;
COPY "Server Today" ("cHostname", "Just A Server Model") FROM STDIN CSV DELIMITER '|';
argnim1 | IBM,9113-550 (P5-550)
argnim2 | IBM,9113-550 (P5-550)
daaa2nim71 | IBM,9131-52A (P5-52A)
daah3nim71 | IBM,8406-71Y (P7-PS701)
hbnim1 | IBM,9133-55A (P5-55A)
\.
TRUNCATE TABLE pgrst_reserved_chars CASCADE;
COPY pgrst_reserved_chars ("*id*", ":arr->ow::cast", "(inside,parens)", "a.dotted.column", " col w space ") FROM STDIN CSV DELIMITER '|';
1 | arrow-1 | parens-1 | dotted-1 | space-1
2 | arrow-2 | parens-2 | dotted-2 | space-2
3 | arrow-3 | parens-3 | dotted-3 | space-3
\.
+3
View File
@@ -0,0 +1,3 @@
set client_min_messages to warning;
DROP SCHEMA IF EXISTS test, private, postgrest, jwt, public, تست, extensions CASCADE;
DROP TYPE IF EXISTS jwt_token CASCADE;
+151
View File
@@ -0,0 +1,151 @@
{
"id": "draft04.json",
"$schema": "draft04.json",
"description": "Core schema meta-schema",
"definitions": {
"schemaArray": {
"type": "array",
"minItems": 1,
"items": { "$ref": "#" }
},
"positiveInteger": {
"type": "integer",
"minimum": 0
},
"positiveIntegerDefault0": {
"allOf": [ { "$ref": "#/definitions/positiveInteger" }, { "default": 0 } ]
},
"simpleTypes": {
"enum": [ "array", "boolean", "integer", "null", "number", "object", "string" ]
},
"stringArray": {
"type": "array",
"items": { "type": "string" },
"minItems": 1,
"uniqueItems": true
}
},
"type": "object",
"properties": {
"id": {
"type": "string",
"format": "uri"
},
"$schema": {
"type": "string",
"format": "uri"
},
"title": {
"type": "string"
},
"description": {
"type": "string"
},
"default": {},
"multipleOf": {
"type": "number",
"minimum": 0,
"exclusiveMinimum": true
},
"maximum": {
"type": "number"
},
"exclusiveMaximum": {
"type": "boolean",
"default": false
},
"minimum": {
"type": "number"
},
"exclusiveMinimum": {
"type": "boolean",
"default": false
},
"maxLength": { "$ref": "#/definitions/positiveInteger" },
"minLength": { "$ref": "#/definitions/positiveIntegerDefault0" },
"pattern": {
"type": "string",
"format": "regex"
},
"additionalItems": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"items": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/schemaArray" }
],
"default": {}
},
"maxItems": { "$ref": "#/definitions/positiveInteger" },
"minItems": { "$ref": "#/definitions/positiveIntegerDefault0" },
"uniqueItems": {
"type": "boolean",
"default": false
},
"maxProperties": { "$ref": "#/definitions/positiveInteger" },
"minProperties": { "$ref": "#/definitions/positiveIntegerDefault0" },
"required": { "$ref": "#/definitions/stringArray" },
"additionalProperties": {
"anyOf": [
{ "type": "boolean" },
{ "$ref": "#" }
],
"default": {}
},
"definitions": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"properties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"patternProperties": {
"type": "object",
"additionalProperties": { "$ref": "#" },
"default": {}
},
"dependencies": {
"type": "object",
"additionalProperties": {
"anyOf": [
{ "$ref": "#" },
{ "$ref": "#/definitions/stringArray" }
]
}
},
"enum": {
"type": "array",
"minItems": 1,
"uniqueItems": true
},
"type": {
"anyOf": [
{ "$ref": "#/definitions/simpleTypes" },
{
"type": "array",
"items": { "$ref": "#/definitions/simpleTypes" },
"minItems": 1,
"uniqueItems": true
}
]
},
"format": { "type": "string" },
"allOf": { "$ref": "#/definitions/schemaArray" },
"anyOf": { "$ref": "#/definitions/schemaArray" },
"oneOf": { "$ref": "#/definitions/schemaArray" },
"not": { "$ref": "#" }
},
"dependencies": {
"exclusiveMaximum": [ "maximum" ],
"exclusiveMinimum": [ "minimum" ]
},
"default": {}
}
+64
View File
@@ -0,0 +1,64 @@
-- From michelp/pgjwt commit c02bbd3
BEGIN;
set client_min_messages to warning;
DROP SCHEMA IF EXISTS jwt CASCADE;
CREATE SCHEMA jwt;
CREATE OR REPLACE FUNCTION jwt.url_encode(data bytea) RETURNS text LANGUAGE sql AS $$
SELECT translate(encode(data, 'base64'), E'+/=\n', '-_');
$$;
CREATE OR REPLACE FUNCTION jwt.url_decode(data text) RETURNS bytea LANGUAGE sql AS $$
WITH t AS (SELECT translate(data, '-_', '+/')),
rem AS (SELECT length((SELECT * FROM t)) % 4) -- compute padding size
SELECT decode(
(SELECT * FROM t) ||
CASE WHEN (SELECT * FROM rem) > 0
THEN repeat('=', (4 - (SELECT * FROM rem)))
ELSE '' END,
'base64');
$$;
CREATE OR REPLACE FUNCTION jwt.algorithm_sign(signables text, secret text, algorithm text)
RETURNS text LANGUAGE sql AS $$
WITH
alg AS (
SELECT CASE
WHEN algorithm = 'HS256' THEN 'sha256'
WHEN algorithm = 'HS384' THEN 'sha384'
WHEN algorithm = 'HS512' THEN 'sha512'
ELSE '' END) -- hmac throws error
SELECT jwt.url_encode(public.hmac(signables, secret, (select * FROM alg)));
$$;
CREATE OR REPLACE FUNCTION jwt.sign(payload json, secret text, algorithm text DEFAULT 'HS256')
RETURNS text LANGUAGE sql AS $$
WITH
header AS (
SELECT jwt.url_encode(convert_to('{"alg":"' || algorithm || '","typ":"JWT"}', 'utf8'))
),
payload AS (
SELECT jwt.url_encode(convert_to(payload::text, 'utf8'))
),
signables AS (
SELECT (SELECT * FROM header) || '.' || (SELECT * FROM payload)
)
SELECT
(SELECT * FROM signables)
|| '.' ||
jwt.algorithm_sign((SELECT * FROM signables), secret, algorithm);
$$;
CREATE OR REPLACE FUNCTION jwt.verify(token text, secret text, algorithm text DEFAULT 'HS256')
RETURNS table(header json, payload json, valid boolean) LANGUAGE sql AS $$
SELECT
convert_from(jwt.url_decode(r[1]), 'utf8')::json AS header,
convert_from(jwt.url_decode(r[2]), 'utf8')::json AS payload,
r[3] = jwt.algorithm_sign(r[1] || '.' || r[2], secret, algorithm) AS valid
FROM regexp_split_to_array(token, '\.') r;
$$;
COMMIT;
+1607
View File
File diff suppressed because it is too large Load Diff
+123
View File
@@ -0,0 +1,123 @@
-- Privileges for anonymous
GRANT USAGE ON SCHEMA
postgrest
, test
, jwt
, public
, "تست"
, extensions
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
, empty_table
, has_count_column
, has_fk
, insertable_view_with_join
, json
, materialized_view
, menagerie
, no_pk
, nullable_integer
, projects
, projects_view
, projects_view_alt
, simple_pk
, tasks
, filtered_tasks
, tsearch
, users
, users_projects
, users_tasks
, "Escap3e;"
, "ghostBusters"
, "withUnique"
, "clashing_column"
, "موارد"
, addresses
, orders
, public.public_consumers
, public.public_orders
, consumers_view
, orders_view
, images
, images_base64
, w_or_wo_comma_names
, items_with_different_col_types
, entities
, child_entities
, grandchild_entities
, ranges
, being
, descendant
, being_part
, part
, leak
, perf_articles
, employees
, tiobe_pls
, only_pk
, family_tree
, organizations
, authors
, books
, forties_books
, fifties_books
, sixties_books
, person
, message
, person_detail
, space
, zone
, projects_dump
, "UnitTest"
, json_arr
, jsonb_test
, authors_books_number
, authors_have_book_in_decade
, forties_and_fifties_books
, odd_years_publications
, foos
, bars
, materialized_projects
, contract
, player_view
, contract_view
, ltree_sample
, isn_sample
, projects_count_grouped_by
, "Server Today"
, pgrst_reserved_chars
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
, leak_id_seq
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;
GRANT SELECT (article_id, user_id) ON TABLE limited_article_stars TO postgrest_test_anonymous;
GRANT INSERT (article_id, user_id) ON TABLE limited_article_stars TO postgrest_test_anonymous;
GRANT UPDATE (article_id, user_id) ON TABLE limited_article_stars TO postgrest_test_anonymous;
REVOKE EXECUTE ON FUNCTION privileged_hello(text) FROM PUBLIC; -- All functions are available to every role(PUBLIC) by default
GRANT EXECUTE ON FUNCTION privileged_hello(text) TO postgrest_test_author;
GRANT USAGE ON SCHEMA test TO postgrest_test_default_role;
+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;
$$;
\set AUTHENTICATOR current_user
DROP ROLE IF EXISTS postgrest_test_anonymous, postgrest_test_default_role, postgrest_test_author;
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 :USER;
+1348 -339
View File
File diff suppressed because it is too large Load Diff
+229
View File
@@ -0,0 +1,229 @@
#!/bin/sh
# Run unit tests for Input/Ouput of PostgREST seen as a black box
# with test output in Test Anything Protocol format.
#
# References:
# [1] Test Anything Protocol
# https://testanything.org/
#
# [2] TAP Specification
# https://testanything.org/tap-specification.html
#
# [3] List of TCP and UDP port numbers
# https://en.wikipedia.org/wiki/List_of_TCP_and_UDP_port_numbers
#
cd "$(dirname "$0")"
cd io-tests
# Port for Test PostgREST Server (must match config)
pgrPort=49421 # in range 4915265535: for private or temporary use
# TAP utilities
currentTest=1
failedTests=0
bailOut(){ echo "Bail out! $1"; exit 1; }
result(){ echo "$1 $currentTest $2"; currentTest=$(( $currentTest + 1 )); }
todo(){ result 'ok' "# TODO: $*"; }
skip(){ result 'ok' "# SKIP: $*"; }
ok(){ result 'ok' "- $1"; }
ko(){ result 'not ok' "- $1"; failedTests=$(( $failedTests + 1 )); }
comment(){ echo "# $1"; }
# Utilities to start/stop test PostgREST server running in the background
pgrStart(){ stack exec -- postgrest "$1" >/dev/null & pgrPID="$!"; }
pgrStartRead(){ stack exec -- postgrest "$1" >/dev/null < "$2" & pgrPID="$!"; }
pgrStarted(){ kill -0 "$pgrPID" 2>/dev/null; }
pgrStop(){ kill "$pgrPID" 2>/dev/null; }
pgrStopAll(){ pkill -f "$(stack path --local-install-root)/bin/postgrest"; }
# Utilities to send HTTP requests to the PostgREST server
rootStatus(){
curl -s -o /dev/null -w '%{http_code}' "http://localhost:$pgrPort/"
}
authorsStatus(){
curl -s -o /dev/null -w '%{http_code}' \
-H "Authorization: Bearer $1" \
"http://localhost:$pgrPort/authors_only"
}
# Start and End of Unit Tests
setUp(){ pgrStopAll; }
cleanUp(){ pgrStopAll; }
# Unit Test Templates
readSecretFromFile(){
case "$1" in
*.b64)
pgrConfig="base64-secret-from-file.config";;
*)
pgrConfig="secret-from-file.config";;
esac
pgrStartRead "./configs/$pgrConfig" "./secrets/$1"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
if pgrStarted
then
authorsJwt="./secrets/${1%.*}.jwt"
httpStatus="$( authorsStatus $(cat "$authorsJwt") )"
if test "$httpStatus" -eq 200
then
ok "authentication with $2 secret read from a file"
else
ko "failed to authenticate using JWT for $2 secret: $httpStatus"
fi
else
ko "failed to read $2 secret from a file"
fi
pgrStop
}
readDbUriFromFile(){
pgrConfig="dburi-from-file.config"
pgrStartRead "./configs/$pgrConfig" "./dburis/$1"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
if pgrStarted
then
ok "connection with $2 dburi read from a file"
else
ko "failed to read $2 dburi from a file"
fi
pgrStop
}
reqWithRoleClaimKey(){
export ROLE_CLAIM_KEY=$1
pgrStart "./configs/role-claim-key.config"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
authorsJwt=$(psql -qtAX postgrest_test -c "select jwt.sign('$2', 'reallyreallyreallyreallyverysafe');")
httpStatus="$( authorsStatus "$authorsJwt" )"
if test "$httpStatus" -eq $3
then
ok "request with \"$1\" role-claim-key for $2 jwt gave $3"
else
ko "request with \"$1\" role-claim-key for $2 jwt gave $httpStatus"
fi
pgrStop
}
invalidRoleClaimKey(){
export ROLE_CLAIM_KEY=$1
pgrStart "./configs/role-claim-key.config"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
if pgrStarted
then
ko "invalid jspath \"$1\" accepted"
else
ok "invalid jspath \"$1\" rejected"
fi
pgrStop
}
# ensure iat claim is successful in the presence of pgrst time cache, see https://github.com/PostgREST/postgrest/issues/1139
ensureIatClaimWorks(){
pgrStart "./configs/simple.config"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
for i in {1..10}; do \
iatJwt=$(psql -qtAX postgrest_test -c "select jwt.sign(row_to_json(r), 'reallyreallyreallyreallyverysafe') from ( select 'postgrest_test_author' as role, extract(epoch from now()) as iat) r")
httpStatus="$( authorsStatus $iatJwt )"
if test "$httpStatus" -ne 200
then
ko "iat claim rejected with $httpStatus"
return
fi
sleep .5;\
done
ok "accepted iat claim"
pgrStop
}
# ensure app settings don't reset on pool timeout of 10 seconds, see https://github.com/PostgREST/postgrest/issues/1141
ensureAppSettings(){
pgrStart "./configs/app-settings.config"
while pgrStarted && test "$( rootStatus )" -ne 200
do
# wait for the server to start
sleep 0.1 \
|| sleep 1 # fallback: subsecond sleep is not standard and may fail
done
sleep 11
response=$(curl -s "http://localhost:$pgrPort/rpc/get_guc_value?name=app.settings.external_api_secret")
if test "$response" = "\"0123456789abcdef\""
then
ok "GET /rpc/get_guc_value response is $response"
else
ko "GET /rpc/get_guc_value response was $response"
fi
pgrStop
}
# PRE: curl must be available
test -n "$(command -v curl)" || bailOut 'curl is not available'
# PRE: postgres must be running
psql -l 1>/dev/null 2>/dev/null || bailOut 'postgres is not running'
setUp
echo "Running IO tests.."
readSecretFromFile word.noeol 'simple (no EOL)'
readSecretFromFile word.txt 'simple'
readSecretFromFile ascii.noeol 'ASCII (no EOL)'
readSecretFromFile ascii.txt 'ASCII'
readSecretFromFile utf8.noeol 'UTF-8 (no EOL)'
readSecretFromFile utf8.txt 'UTF-8'
readSecretFromFile binary.noeol 'binary'
readSecretFromFile binary.eol 'binary (+EOL)'
readSecretFromFile word.b64 'Base64 (simple)'
readSecretFromFile ascii.b64 'Base64 (ASCII)'
readSecretFromFile utf8.b64 'Base64 (UTF-8)'
readSecretFromFile binary.b64 'Base64 (binary)'
readDbUriFromFile uri.noeol "(no EOL)"
readDbUriFromFile uri.txt "(EOL)"
reqWithRoleClaimKey '.postgrest.a_role' '{"postgrest":{"a_role":"postgrest_test_author"}}' 200
reqWithRoleClaimKey '.customObject.manyRoles[1]' '{"customObject":{"manyRoles": ["other", "postgrest_test_author"]}}' 200
reqWithRoleClaimKey '."https://www.example.com/roles"[0].value' '{"https://www.example.com/roles":[{"value":"postgrest_test_author"}]}' 200
reqWithRoleClaimKey '.myDomain[3]' '{"myDomain":["other","postgrest_test_author"]}' 401
reqWithRoleClaimKey '.myRole' '{"role":"postgrest_test_author"}' 401
invalidRoleClaimKey 'role.other'
invalidRoleClaimKey '.role##'
invalidRoleClaimKey '.my_role;;domain'
invalidRoleClaimKey '.#$%&$%/'
invalidRoleClaimKey ''
invalidRoleClaimKey 1234
ensureIatClaimWorks
ensureAppSettings
cleanUp
exit $failedTests
@@ -0,0 +1,8 @@
db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
app.settings.external_api_secret = "0123456789abcdef"
@@ -0,0 +1,10 @@
db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
# Read secret from a file: /dev/stdin (alias for standard input)
jwt-secret = "@/dev/stdin"
secret-is-base64 = true
@@ -0,0 +1,7 @@
db-uri = "@/dev/stdin"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
jwt-secret = "reallyreallyreallyreallyverysafe"
@@ -0,0 +1,8 @@
db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
role-claim-key = "$(ROLE_CLAIM_KEY)"
jwt-secret = "reallyreallyreallyreallyverysafe"
@@ -0,0 +1,10 @@
db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
# Read secret from a file: /dev/stdin (alias for standard input)
jwt-secret = "@/dev/stdin"
secret-is-base64 = false
+7
View File
@@ -0,0 +1,7 @@
db-uri = "postgres:///postgrest_test"
db-schema = "test"
db-anon-role = "postgrest_test_anonymous"
db-pool = 1
server-host = "127.0.0.1"
server-port = 49421
jwt-secret = "reallyreallyreallyreallyverysafe"
+1
View File
@@ -0,0 +1 @@
postgres:///postgrest_test
+1
View File
@@ -0,0 +1 @@
postgres:///postgrest_test
+1
View File
@@ -0,0 +1 @@
QUJDCkVhc3kgYXMKMTIzCk9yIHNpbXBsZSBhcwpEbyByZSBtaQ==
+1
View File
@@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.p__cvqgrtteKUhxrJwTwqxeuFmlEm3hEj1mk7yO15M4
+5
View File
@@ -0,0 +1,5 @@
ABC
Easy as
123
Or simple as
Do re mi
+5
View File
@@ -0,0 +1,5 @@
ABC
Easy as
123
Or simple as
Do re mi
+1
View File
@@ -0,0 +1 @@
RTwSHLM0/PWM2YCOyBiyChMQQamZLTZGrXdzGk61o5A=
+2
View File
@@ -0,0 +1,2 @@
E<³4üõŒÙ€ŽÈ²
A©™-6F­wsNµ£
+1
View File
@@ -0,0 +1 @@
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoicG9zdGdyZXN0X3Rlc3RfYXV0aG9yIn0.zyXYJKXgVYaWqMYgUXMk4zqXSU5cwA_vK2z9_5lRlqA

Some files were not shown because too many files have changed in this diff Show More